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 a40005814a..813b8d0f4f 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -4,6 +4,10 @@ import { ePeopleRegistryReducer, EPeopleRegistryState } from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; +import { + groupRegistryReducer, + GroupRegistryState +} from './+admin/admin-access-control/group-registry/group-registry.reducers'; import { metadataRegistryReducer, MetadataRegistryState @@ -47,6 +51,7 @@ export interface AppState { relationshipLists: NameVariantListsState; communityList: CommunityListState; epeopleRegistry: EPeopleRegistryState; + groupRegistry: GroupRegistryState; } export const appReducers: ActionReducerMap = { @@ -66,6 +71,7 @@ export const appReducers: ActionReducerMap = { relationshipLists: nameVariantReducer, communityList: CommunityListReducer, epeopleRegistry: ePeopleRegistryReducer, + groupRegistry: groupRegistryReducer, }; export const routerStateSelector = (state: AppState) => state.router; @@ -79,3 +85,10 @@ export function keySelector(key: string, selector): MemoizedSelector { imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } })], providers: [ 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..6b7ab2bd77 100644 --- a/src/app/community-list-page/community-list-service.spec.ts +++ b/src/app/community-list-page/community-list-service.spec.ts @@ -2,7 +2,7 @@ import { of as observableOf } from 'rxjs'; import { TestBed, inject, async } from '@angular/core/testing'; import { Store } from '@ngrx/store'; import { AppState } from '../app.reducer'; -import { MockStore } from '../shared/testing/mock-store'; +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'; @@ -11,110 +11,121 @@ import { CommunityDataService } from '../core/data/community-data.service'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ -} from '../shared/testing/utils'; +} from '../shared/remote-data.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'; 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 +194,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(); @@ -316,7 +327,10 @@ 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(() => { @@ -469,18 +483,19 @@ 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(() => { + 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; 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..a91c5fa057 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'; @@ -199,7 +199,7 @@ describe('CommunityListComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock }, }), CdkTreeModule, diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index e5c9210769..465fb69dd2 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -3,7 +3,6 @@ import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators import { Inject, 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'; @@ -17,8 +16,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 +67,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.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 1f6fa51afd..e231857159 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -27,11 +27,12 @@ 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 { AuthStatus } from './models/auth-status.model'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; describe('AuthEffects', () => { let authEffects: AuthEffects; diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 72b0cc2616..f5ac0c4361 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`, () => { diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 7a39ef3da4..1606ed9185 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'; diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index 03759987bf..89d8cdce4e 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -9,25 +9,26 @@ 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 { 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 }, 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-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/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..88d1890de2 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'; 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 fd398f2971..7d57bb4433 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,28 +1,20 @@ 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 { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -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'; import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { RequestService } from '../data/request.service'; -import { PatchRequest, PutRequest } from '../data/request.models'; +import { PatchRequest } from '../data/request.models'; import { ObjectCacheService } from './object-cache.service'; import { ApplyPatchObjectCacheAction } from './object-cache.actions'; -import { GenericConstructor } from '../shared/generic-constructor'; 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'; @@ -39,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), @@ -87,7 +79,7 @@ export class ServerSyncBufferEffects { return observableOf({ type: 'NO_ACTION' }); } }) - ) + ); }) ); @@ -116,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/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts index c86a0d5654..d79dd51da4 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -68,6 +68,8 @@ function addToServerSyncQueue(state: ServerSyncBufferState, action: AddToSSBActi const actionEntry = action.payload as ServerSyncBufferEntry; if (hasNoValue(state.buffer.find((entry) => entry.href === actionEntry.href && entry.method === actionEntry.method))) { return Object.assign({}, state, { buffer: state.buffer.concat(actionEntry) }); + } else { + return state; } } 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 c2d14663e6..e073b3f995 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -3,17 +3,21 @@ import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; 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'; import { HostWindowService } from '../shared/host-window.service'; import { MenuService } from '../shared/menu/menu.service'; import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service'; -import { MOCK_RESPONSE_MAP, MockResponseMap, mockResponseMap } from '../shared/mocks/dspace-rest-v2/mocks/mock-response-map'; +import { + MOCK_RESPONSE_MAP, + ResponseMapMock, + mockResponseMap +} 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'; @@ -39,7 +43,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'; @@ -130,6 +134,10 @@ import { PoolTask } from './tasks/models/pool-task-object.model'; import { TaskObject } from './tasks/models/task-object.model'; 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'; @@ -139,22 +147,24 @@ import { Process } from '../process-page/processes/process.model'; import { ProcessDataService } from './data/processes/process-data.service'; import { ScriptDataService } from './data/processes/script-data.service'; import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service'; +import { WorkflowActionDataService } from './data/workflow-action-data.service'; +import { WorkflowAction } from './tasks/models/workflow-action-object.model'; /** * 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) ]; @@ -172,11 +182,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, @@ -233,6 +239,7 @@ const PROVIDERS = [ DSpaceObjectDataService, DSOChangeAnalyzer, DefaultChangeAnalyzer, + ArrayMoveChangeAnalyzer, ObjectSelectService, CSSVariableService, MenuService, @@ -244,6 +251,7 @@ const PROVIDERS = [ TaskResponseParsingService, ClaimedTaskDataService, PoolTaskDataService, + BitstreamDataService, EntityTypeService, ContentSourceResponseParsingService, SearchService, @@ -259,6 +267,7 @@ const PROVIDERS = [ VersionHistoryDataService, LicenseDataService, ItemTypeDataService, + WorkflowActionDataService, ProcessDataService, ScriptDataService, ProcessFilesResponseParsingService, @@ -313,7 +322,8 @@ export const models = Script, Process, Version, - VersionHistory + VersionHistory, + WorkflowAction ]; @NgModule({ diff --git a/src/app/core/data/array-move-change-analyzer.service.spec.ts b/src/app/core/data/array-move-change-analyzer.service.spec.ts new file mode 100644 index 0000000000..5f5388d935 --- /dev/null +++ b/src/app/core/data/array-move-change-analyzer.service.spec.ts @@ -0,0 +1,107 @@ +import { ArrayMoveChangeAnalyzer } from './array-move-change-analyzer.service'; +import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { Operation } from 'fast-json-patch'; + +/** + * Helper class for creating move tests + * Define a "from" and "to" index to move objects within the array before comparing + */ +class MoveTest { + from: number; + to: number; + + constructor(from: number, to: number) { + this.from = from; + this.to = to; + } +} + +describe('ArrayMoveChangeAnalyzer', () => { + const comparator = new ArrayMoveChangeAnalyzer(); + + let originalArray = []; + + describe('when all values are defined', () => { + beforeEach(() => { + originalArray = [ + '98700118-d65d-4636-b1d0-dba83fc932e1', + '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', + 'e56eb99e-2f7c-4bee-9b3f-d3dcc83386b1', + '0f608168-cdfc-46b0-92ce-889f7d3ac684', + '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + ]; + }); + + testMove([ + { op: 'move', from: '/2', path: '/4' }, + ], new MoveTest(2, 4)); + + testMove([ + { op: 'move', from: '/0', path: '/3' }, + ], new MoveTest(0, 3)); + + testMove([ + { op: 'move', from: '/0', path: '/3' }, + { op: 'move', from: '/2', path: '/1' } + ], new MoveTest(0, 3), new MoveTest(1, 2)); + + testMove([ + { op: 'move', from: '/0', path: '/1' }, + { op: 'move', from: '/3', path: '/4' } + ], new MoveTest(0, 1), new MoveTest(3, 4)); + + testMove([], new MoveTest(0, 4), new MoveTest(4, 0)); + + testMove([ + { op: 'move', from: '/0', path: '/3' }, + { op: 'move', from: '/2', path: '/1' } + ], new MoveTest(0, 4), new MoveTest(1, 3), new MoveTest(2, 4)); + }); + + describe('when some values are undefined (index 2 and 3)', () => { + beforeEach(() => { + originalArray = [ + '98700118-d65d-4636-b1d0-dba83fc932e1', + '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', + undefined, + undefined, + '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + ]; + }); + + // It can't create a move operation for undefined values, so it should create move operations for the defined values instead + testMove([ + { op: 'move', from: '/4', path: '/3' }, + ], new MoveTest(2, 4)); + + // Moving a defined value should result in the same operations + testMove([ + { op: 'move', from: '/0', path: '/3' }, + ], new MoveTest(0, 3)); + }); + + /** + * Helper function for creating a move test + * + * @param expectedOperations An array of expected operations after comparing the original array with the array + * created using the provided MoveTests + * @param moves An array of MoveTest objects telling the test where to move objects before comparing + */ + function testMove(expectedOperations: Operation[], ...moves: MoveTest[]) { + describe(`move ${moves.map((move) => `${move.from} to ${move.to}`).join(' and ')}`, () => { + let result; + + beforeEach(() => { + const movedArray = [...originalArray]; + moves.forEach((move) => { + moveItemInArray(movedArray, move.from, move.to); + }); + result = comparator.diff(originalArray, movedArray); + }); + + it('should create the expected move operations', () => { + expect(result).toEqual(expectedOperations); + }); + }); + } +}); diff --git a/src/app/core/data/array-move-change-analyzer.service.ts b/src/app/core/data/array-move-change-analyzer.service.ts new file mode 100644 index 0000000000..39d22fc463 --- /dev/null +++ b/src/app/core/data/array-move-change-analyzer.service.ts @@ -0,0 +1,37 @@ +import { MoveOperation } from 'fast-json-patch/lib/core'; +import { Injectable } from '@angular/core'; +import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { hasValue } from '../../shared/empty.util'; + +/** + * A class to determine move operations between two arrays + */ +@Injectable() +export class ArrayMoveChangeAnalyzer { + + /** + * Compare two arrays detecting and returning move operations + * + * @param array1 The original array + * @param array2 The custom array to compare with the original + */ + diff(array1: T[], array2: T[]): MoveOperation[] { + const result = []; + const moved = [...array1]; + array1.forEach((value: T, index: number) => { + if (hasValue(value)) { + const otherIndex = array2.indexOf(value); + const movedIndex = moved.indexOf(value); + if (index !== otherIndex && movedIndex !== otherIndex) { + moveItemInArray(moved, movedIndex, otherIndex); + result.push(Object.assign({ + op: 'move', + from: '/' + movedIndex, + path: '/' + otherIndex + }) as MoveOperation) + } + } + }); + return result; + } +} 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 new file mode 100644 index 0000000000..b328141d7b --- /dev/null +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -0,0 +1,58 @@ +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 { 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; + let objectCache: ObjectCacheService; + let requestService: RequestService; + let halService: HALEndpointService; + let bitstreamFormatService: BitstreamFormatDataService; + const bitstreamFormatHref = 'rest-api/bitstreamformats'; + + const bitstream = Object.assign(new Bitstream(), { + uuid: 'fake-bitstream', + _links: { + self: { href: 'fake-bitstream-self' } + } + }); + const format = Object.assign(new BitstreamFormat(), { + id: '2', + shortDescription: 'PNG', + description: 'Portable Network Graphics', + supportLevel: BitstreamFormatSupportLevel.Known + }); + const url = 'fake-bitstream-url'; + + beforeEach(() => { + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + requestService = getMockRequestService(); + halService = Object.assign(new HALEndpointServiceStub(url)); + bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', { + getBrowseEndpoint: observableOf(bitstreamFormatHref) + }); + + service = new BitstreamDataService(requestService, null, null, null, objectCache, halService, null, null, null, null, bitstreamFormatService); + }); + + describe('when updating the bitstream\'s format', () => { + beforeEach(() => { + service.updateFormat(bitstream, format); + }); + + it('should configure a put request', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); + }); + }); +}); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index c571c7f96c..4c24f5d78b 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -1,8 +1,8 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/internal/Observable'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -22,8 +22,14 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; import { RemoteDataError } from './remote-data-error'; -import { FindListOptions } from './request.models'; +import { FindListOptions, PutRequest } from './request.models'; import { RequestService } from './request.service'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { RestResponse } from '../cache/response.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { combineLatest as observableCombineLatest } from 'rxjs'; /** * A service to retrieve {@link Bitstream}s from the REST API @@ -50,6 +56,7 @@ export class BitstreamDataService extends DataService { protected http: HttpClient, protected comparator: DSOChangeAnalyzer, protected bundleService: BundleDataService, + protected bitstreamFormatService: BitstreamFormatDataService ) { super(); } @@ -167,4 +174,37 @@ export class BitstreamDataService extends DataService { ); } + /** + * Set the format of a bitstream + * @param bitstream + * @param format + */ + updateFormat(bitstream: Bitstream, format: BitstreamFormat): Observable { + const requestId = this.requestService.generateRequestId(); + const bitstreamHref$ = this.getBrowseEndpoint().pipe( + map((href: string) => `${href}/${bitstream.id}`), + switchMap((href: string) => this.halService.getEndpoint('format', href)) + ); + const formatHref$ = this.bitstreamFormatService.getBrowseEndpoint().pipe( + map((href: string) => `${href}/${format.id}`) + ); + observableCombineLatest([bitstreamHref$, formatHref$]).pipe( + map(([bitstreamHref, formatHref]) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + return new PutRequest(requestId, bitstreamHref, formatHref, options); + }), + configureRequest(this.requestService), + take(1) + ).subscribe(() => { + this.requestService.removeByHrefSubstring(bitstream.self + '/format'); + }); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + } + } 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 64d58eb8ec..160ea0ff0d 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/internal/Observable'; -import { map } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -18,8 +18,10 @@ import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; -import { FindListOptions } from './request.models'; +import { FindListOptions, GetRequest } from './request.models'; import { RequestService } from './request.service'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { Bitstream } from '../shared/bitstream.model'; /** * A service to retrieve {@link Bundle}s from the REST API @@ -30,6 +32,7 @@ import { RequestService } from './request.service'; @dataService(BUNDLE) export class BundleDataService extends DataService { protected linkPath = 'bundles'; + protected bitstreamsEndpoint = 'bitstreams'; constructor( protected requestService: RequestService, @@ -81,4 +84,34 @@ export class BundleDataService extends DataService { }), ); } + + /** + * Get the bitstreams endpoint for a bundle + * @param bundleId + */ + getBitstreamsEndpoint(bundleId: string): Observable { + return this.getBrowseEndpoint().pipe( + switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)) + ); + } + + /** + * Get a bundle's bitstreams using paginated search options + * @param bundleId The bundle's ID + * @param searchOptions The search options to use + * @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) + ); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs, ...linksToFollow); + } } diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 96141d6a8a..eb3dabf195 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'; 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 c370be2b9e..a99fc54269 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -2,24 +2,22 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { compare, Operation } from 'fast-json-patch'; import { Observable, of as observableOf } from 'rxjs'; -import * as uuidv4 from 'uuid/v4'; 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'; @@ -55,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, @@ -85,7 +92,9 @@ describe('DataService', () => { ); } - service = initTestService(); + beforeEach(() => { + service = initTestService(); + }) describe('getFindAllHref', () => { @@ -152,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); }); }); @@ -228,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 135834b430..7cbfb2ad03 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -14,7 +14,7 @@ import { take, tap } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -44,7 +44,8 @@ import { FindByIDRequest, FindListOptions, FindListRequest, - GetRequest, PatchRequest + GetRequest, + PatchRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; @@ -475,6 +476,39 @@ export abstract class DataService { * @return an observable that emits true when the deletion was successful, false when it failed */ delete(dsoID: string, copyVirtualMetadata?: string[]): Observable { + const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); + } + + /** + * Delete an existing DSpace Object on the server + * @param dsoID The DSpace Object' id to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * Return an observable of the completed response + */ + deleteAndReturnResponse(dsoID: string, copyVirtualMetadata?: string[]): Observable { + const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); + + return this.requestService.getByUUID(requestId).pipe( + hasValueOperator(), + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); + } + + /** + * Delete an existing DSpace Object on the server + * @param dsoID The DSpace Object' id to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * Return the delete request's ID + */ + private deleteAndReturnRequestId(dsoID: string, copyVirtualMetadata?: string[]): string { const requestId = this.requestService.generateRequestId(); const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( @@ -495,10 +529,7 @@ export abstract class DataService { }) ).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response.isSuccessful) - ); + return requestId; } /** 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 06adfd5143..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'; @@ -47,6 +47,9 @@ describe('ItemDataService', () => { return cold('a', { a: itemEndpoint }); } } as HALEndpointService; + const bundleService = jasmine.createSpyObj('bundleService', { + findByHref: {} + }); const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; const options = Object.assign(new FindListOptions(), { @@ -87,7 +90,8 @@ describe('ItemDataService', () => { halEndpointService, notificationsService, http, - comparator + comparator, + bundleService ); } @@ -212,4 +216,20 @@ describe('ItemDataService', () => { }); }); + describe('createBundle', () => { + const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; + const bundleName = 'ORIGINAL'; + let result; + + beforeEach(() => { + service = initTestService(); + spyOn(requestService, 'configure'); + result = service.createBundle(itemId, bundleName); + }); + + it('should configure a POST request', () => { + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); + }); + }); + }); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index a23eb27f4a..562050c802 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, map, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { BrowseService } from '../browse/browse.service'; @@ -32,6 +32,7 @@ import { RemoteData } from './remote-data'; import { DeleteRequest, FindListOptions, + GetRequest, MappedCollectionsRequest, PatchRequest, PostRequest, @@ -40,6 +41,10 @@ import { } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { Bundle } from '../shared/bundle.model'; +import { MetadataMap } from '../shared/metadata.models'; +import { BundleDataService } from './bundle-data.service'; @Injectable() @dataService(ITEM) @@ -56,6 +61,7 @@ export class ItemDataService extends DataService { protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOChangeAnalyzer, + protected bundleService: BundleDataService ) { super(); } @@ -219,6 +225,76 @@ export class ItemDataService extends DataService { ); } + /** + * Get the endpoint for an item's bundles + * @param itemId + */ + public getBundlesEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((url: string) => this.halService.getEndpoint('bundles', `${url}/${itemId}`)) + ); + } + + /** + * Get an item's bundles using paginated search options + * @param itemId The item's ID + * @param searchOptions The search options to use + */ + public getBundles(itemId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + const hrefObs = this.getBundlesEndpoint(itemId).pipe( + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + ); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs); + } + + /** + * Create a new bundle on an item + * @param itemId The item's ID + * @param bundleName The new bundle's name + * @param metadata Optional metadata for the bundle + */ + public createBundle(itemId: string, bundleName: string, metadata?: MetadataMap): Observable> { + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getBundlesEndpoint(itemId); + + const bundleJson = { + name: bundleName, + metadata: metadata ? metadata : {} + }; + + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/json'); + options.headers = headers; + const request = new PostRequest(requestId, href, JSON.stringify(bundleJson), options); + this.requestService.configure(request); + }); + + const selfLink$ = this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: any) => { + if (isNotEmpty(response.resourceSelfLinks)) { + return response.resourceSelfLinks[0]; + } + }), + distinctUntilChanged() + ) as Observable; + + return selfLink$.pipe( + switchMap((selfLink: string) => this.bundleService.findByHref(selfLink)), + ); + } + /** * Get the endpoint to move the item * @param itemId 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/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index 9df9acec8f..94918157ee 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -8,6 +8,7 @@ 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'), @@ -15,7 +16,9 @@ export const ObjectUpdatesActionTypes = { DISCARD: type('dspace/core/cache/object-updates/DISCARD'), 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'), }; /* tslint:disable:max-classes-per-file */ @@ -26,7 +29,8 @@ export const ObjectUpdatesActionTypes = { export enum FieldChangeType { UPDATE = 0, ADD = 1, - REMOVE = 2 + REMOVE = 2, + MOVE = 3 } /** @@ -37,7 +41,10 @@ export class InitializeFieldsAction implements Action { payload: { url: string, fields: Identifiable[], - lastModified: Date + lastModified: Date, + order: string[], + pageSize: number, + page: number }; /** @@ -47,13 +54,49 @@ export class InitializeFieldsAction implements Action { * the unique url of the page for which the fields are being initialized * @param fields The identifiable fields of which the updates are kept track of * @param lastModified The last modified date of the object that belongs to the page + * @param order A custom order to keep track of objects moving around + * @param pageSize The page size used to fill empty pages for the custom order + * @param page The first page to populate in the custom order */ constructor( url: string, fields: Identifiable[], - lastModified: Date + lastModified: Date, + order: string[] = [], + pageSize: number = 9999, + page: number = 0 ) { - this.payload = { url, fields, lastModified }; + 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 }; } } @@ -180,7 +223,8 @@ export class DiscardObjectUpdatesAction implements Action { type = ObjectUpdatesActionTypes.DISCARD; payload: { url: string, - notification: INotification + notification: INotification, + discardAll: boolean; }; /** @@ -189,12 +233,14 @@ export class DiscardObjectUpdatesAction implements Action { * @param url * the unique url of the page for which the changes should be discarded * @param notification The notification that is raised when changes are discarded + * @param discardAll discard all */ constructor( url: string, - notification: INotification + notification: INotification, + discardAll = false ) { - this.payload = { url, notification }; + this.payload = { url, notification, discardAll }; } } @@ -242,6 +288,13 @@ export class RemoveObjectUpdatesAction implements Action { } } +/** + * An ngrx action to remove all previously discarded updates in the ObjectUpdates state + */ +export class RemoveAllObjectUpdatesAction implements Action { + type = ObjectUpdatesActionTypes.REMOVE_ALL; +} + /** * An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid */ @@ -267,6 +320,43 @@ 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 */ /** @@ -279,6 +369,9 @@ export type ObjectUpdatesAction | ReinstateObjectUpdatesAction | RemoveObjectUpdatesAction | RemoveFieldUpdateAction + | MoveFieldUpdateAction + | AddPageToCustomOrderAction + | RemoveAllObjectUpdatesAction | SelectVirtualMetadataAction | SetEditableFieldUpdateAction | SetValidFieldUpdateAction; diff --git a/src/app/core/data/object-updates/object-updates.effects.ts b/src/app/core/data/object-updates/object-updates.effects.ts index 88cd3bc718..239fee9477 100644 --- a/src/app/core/data/object-updates/object-updates.effects.ts +++ b/src/app/core/data/object-updates/object-updates.effects.ts @@ -3,12 +3,12 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { DiscardObjectUpdatesAction, ObjectUpdatesAction, - ObjectUpdatesActionTypes, + ObjectUpdatesActionTypes, RemoveAllObjectUpdatesAction, RemoveObjectUpdatesAction } from './object-updates.actions'; import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { of as observableOf, race as observableRace, Subject } from 'rxjs'; -import { hasNoValue } from '../../../shared/empty.util'; +import { hasNoValue, hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { INotification } from '../../../shared/notifications/models/notification.model'; import { @@ -16,6 +16,7 @@ import { NotificationsActionTypes, RemoveNotificationAction } from '../../../shared/notifications/notifications.actions'; +import { Action } from '@ngrx/store'; /** * NGRX effects for ObjectUpdatesActions @@ -53,13 +54,14 @@ export class ObjectUpdatesEffects { .pipe( ofType(...Object.values(ObjectUpdatesActionTypes)), map((action: ObjectUpdatesAction) => { - const url: string = action.payload.url; + if (hasValue((action as any).payload)) { + const url: string = (action as any).payload.url; if (hasNoValue(this.actionMap$[url])) { this.actionMap$[url] = new Subject(); } this.actionMap$[url].next(action); } - ) + }) ); /** @@ -91,9 +93,15 @@ export class ObjectUpdatesEffects { const url: string = action.payload.url; const notification: INotification = action.payload.notification; const timeOut = notification.options.timeOut; + + let removeAction: Action = new RemoveObjectUpdatesAction(action.payload.url); + if (action.payload.discardAll) { + removeAction = new RemoveAllObjectUpdatesAction(); + } + return observableRace( // Either wait for the delay and perform a remove action - observableOf(new RemoveObjectUpdatesAction(action.payload.url)).pipe(delay(timeOut)), + observableOf(removeAction).pipe(delay(timeOut)), // Or wait for a a user action this.actionMap$[url].pipe( take(1), @@ -106,19 +114,19 @@ export class ObjectUpdatesEffects { return { type: 'NO_ACTION' } } // If someone performed another action, assume the user does not want to reinstate and remove all changes - return new RemoveObjectUpdatesAction(action.payload.url); + return removeAction }) ), this.notificationActionMap$[notification.id].pipe( filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_NOTIFICATION), map(() => { - return new RemoveObjectUpdatesAction(action.payload.url); + return removeAction; }) ), this.notificationActionMap$[this.allIdentifier].pipe( filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS), map(() => { - return new RemoveObjectUpdatesAction(action.payload.url); + return removeAction; }) ) ) 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 faae4732bc..bdf202049e 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,10 +1,10 @@ import * as deepFreeze from 'deep-freeze'; import { - AddFieldUpdateAction, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, - ReinstateObjectUpdatesAction, + InitializeFieldsAction, MoveFieldUpdateAction, + ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction, RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction } from './object-updates.actions'; @@ -85,6 +85,16 @@ 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 + } } }; @@ -111,6 +121,16 @@ 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: { @@ -145,6 +165,16 @@ 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 + } } }; @@ -213,7 +243,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); + const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate, [identifiable1.uuid, identifiable3.uuid], 10, 0); const expectedState = { [url]: { @@ -231,7 +261,17 @@ describe('objectUpdatesReducer', () => { }, fieldUpdates: {}, virtualMetadataSources: {}, - lastModified: modDate + lastModified: modDate, + customOrder: { + initialOrderPages: [ + { order: [identifiable1.uuid, identifiable3.uuid] } + ], + newOrderPages: [ + { order: [identifiable1.uuid, identifiable3.uuid] } + ], + pageSize: 10, + changed: false + } } }; const newState = objectUpdatesReducer(testState, action); @@ -283,10 +323,44 @@ describe('objectUpdatesReducer', () => { expect(newState[url + OBJECT_UPDATES_TRASH_PATH]).toBeUndefined(); }); + it('should remove all updates from the state when the REMOVE_ALL action is dispatched', () => { + const action = new RemoveAllObjectUpdatesAction(); + + const newState = objectUpdatesReducer(discardedTestState, action as any); + expect(newState[url].fieldUpdates).toBeUndefined(); + expect(newState[url + OBJECT_UPDATES_TRASH_PATH]).toBeUndefined(); + }); + it('should remove a given field\'s update from the state when the REMOVE_FIELD action is dispatched, based on the payload', () => { const action = new RemoveFieldUpdateAction(url, uuid); 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 cffd41856d..759a9f5c87 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, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, + InitializeFieldsAction, MoveFieldUpdateAction, ObjectUpdatesAction, ObjectUpdatesActionTypes, ReinstateObjectUpdatesAction, @@ -12,7 +12,9 @@ import { SetValidFieldUpdateAction, SelectVirtualMetadataAction, } from './object-updates.actions'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; +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 {Relationship} from '../../shared/item-relationships/relationship.model'; /** @@ -46,7 +48,7 @@ export interface Identifiable { /** * The state of a single field update */ -export interface FieldUpdate { +export interface FieldUpdate { field: Identifiable, changeType: FieldChangeType } @@ -81,6 +83,20 @@ 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 */ @@ -89,6 +105,7 @@ export interface ObjectUpdatesEntry { fieldUpdates: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; + customOrder: CustomOrder } /** @@ -121,6 +138,9 @@ 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); } @@ -136,6 +156,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates case ObjectUpdatesActionTypes.REMOVE: { return removeObjectUpdates(state, action as RemoveObjectUpdatesAction); } + case ObjectUpdatesActionTypes.REMOVE_ALL: { + return removeAllObjectUpdates(state); + } case ObjectUpdatesActionTypes.REMOVE_FIELD: { return removeFieldUpdate(state, action as RemoveFieldUpdateAction); } @@ -145,6 +168,9 @@ 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; } @@ -160,18 +186,50 @@ 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 } + { lastModified: lastModifiedServer }, + { customOrder: { + initialOrderPages: initialOrderPages, + newOrderPages: initialOrderPages, + pageSize: pageSize, + changed: false } + } ); 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 @@ -252,7 +310,24 @@ function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) * @param action The action to perform on the current state */ function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) { - const url: string = action.payload.url; + if (action.payload.discardAll) { + let newState = Object.assign({}, state); + Object.keys(state).filter((path: string) => !path.endsWith(OBJECT_UPDATES_TRASH_PATH)).forEach((path: string) => { + newState = discardObjectUpdatesFor(path, newState); + }); + return newState; + } else { + const url: string = action.payload.url; + return discardObjectUpdatesFor(url, state); + } +} + +/** + * Discard all updates for a specific action's url in the store + * @param url The action's url + * @param state The current state + */ +function discardObjectUpdatesFor(url: string, state: any) { const pageState: ObjectUpdatesEntry = state[url]; const newFieldStates = {}; Object.keys(pageState.fieldStates).forEach((uuid: string) => { @@ -263,9 +338,19 @@ function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) { } }); + 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 + fieldStates: newFieldStates, + customOrder: newCustomOrder }); return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState }); } @@ -305,6 +390,18 @@ function removeObjectUpdatesByURL(state: any, url: string) { return newState; } +/** + * Remove all updates in the store + * @param state The current state + */ +function removeAllObjectUpdates(state: any) { + const newState = Object.assign({}, state); + Object.keys(state).filter((path: string) => path.endsWith(OBJECT_UPDATES_TRASH_PATH)).forEach((path: string) => { + delete newState[path]; + }); + return newState; +} + /** * Discard the update for a specific action's url and field UUID in the store * @param state The current state @@ -407,3 +504,121 @@ 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 730ee5ad43..780a402a84 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,6 +2,7 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../../core.reducers'; import { ObjectUpdatesService } from './object-updates.service'; import { + AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, @@ -12,6 +13,8 @@ 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; @@ -44,7 +47,7 @@ describe('ObjectUpdatesService', () => { }; store = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); - service = (new ObjectUpdatesService(store)); + service = new ObjectUpdatesService(store, new ArrayMoveChangeAnalyzer()); spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry)); spyOn(service as any, 'getFieldState').and.callFake((uuid) => { @@ -60,6 +63,25 @@ 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); @@ -77,6 +99,66 @@ describe('ObjectUpdatesService', () => { }); }); + describe('getFieldUpdatesExclusive', () => { + it('should return the list of all fields, including their update if there is one, excluding updates that aren\'t part of the initial values provided', (done) => { + const result$ = service.getFieldUpdatesExclusive(url, identifiables); + expect((service as any).getObjectEntry).toHaveBeenCalledWith(url); + + const expectedResult = { + [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, + [identifiable2.uuid]: { field: identifiable2, changeType: undefined } + }; + + result$.subscribe((result) => { + expect(result).toEqual(expectedResult); + done(); + }); + }); + }); + + 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); @@ -192,7 +274,11 @@ describe('ObjectUpdatesService', () => { }); describe('when updates are emtpy', () => { beforeEach(() => { - (service as any).getObjectEntry.and.returnValue(observableOf({})) + (service as any).getObjectEntry.and.returnValue(observableOf({ + customOrder: { + changed: false + } + })) }); it('should return false when there are no updates', () => { @@ -259,4 +345,45 @@ describe('ObjectUpdatesService', () => { expect(store.dispatch).toHaveBeenCalledWith(new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true)); }); }); + + 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 367b73ee30..c9a7f47e81 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -8,15 +8,16 @@ import { Identifiable, OBJECT_UPDATES_TRASH_PATH, ObjectUpdatesEntry, - ObjectUpdatesState, + ObjectUpdatesState, OrderPage, VirtualMetadataSource } from './object-updates.reducer'; import { Observable } from 'rxjs'; import { - AddFieldUpdateAction, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, + MoveFieldUpdateAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, @@ -26,6 +27,9 @@ import { import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { hasNoValue, hasValue, isEmpty, isNotEmpty } 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']); @@ -48,7 +52,8 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel */ @Injectable() export class ObjectUpdatesService { - constructor(private store: Store) { + constructor(private store: Store, + private comparator: ArrayMoveChangeAnalyzer) { } @@ -62,6 +67,28 @@ 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 @@ -94,14 +121,15 @@ export class ObjectUpdatesService { * 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 ignoreStates Ignore the fieldStates to loop over the fieldUpdates instead */ - getFieldUpdates(url: string, initialFields: Identifiable[]): Observable { + getFieldUpdates(url: string, initialFields: Identifiable[], ignoreStates?: boolean): Observable { const objectUpdates = this.getObjectEntry(url); return objectUpdates.pipe( switchMap((objectEntry) => { const fieldUpdates: FieldUpdates = {}; if (hasValue(objectEntry)) { - Object.keys(objectEntry.fieldStates).forEach((uuid) => { + Object.keys(ignoreStates ? objectEntry.fieldUpdates : objectEntry.fieldStates).forEach((uuid) => { fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid]; }); } @@ -138,6 +166,31 @@ 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 @@ -207,6 +260,19 @@ 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 @@ -264,6 +330,15 @@ export class ObjectUpdatesService { this.store.dispatch(new DiscardObjectUpdatesAction(url, undoNotification)); } + /** + * Method to dispatch a DiscardObjectUpdatesAction to the store with discardAll set to true + * @param url The page's URL for which the changes should be discarded + * @param undoNotification The notification which is should possibly be canceled + */ + discardAllFieldUpdates(url: string, undoNotification: INotification) { + this.store.dispatch(new DiscardObjectUpdatesAction(url, undoNotification, true)); + } + /** * Method to dispatch an ReinstateObjectUpdatesAction to the store * @param url The page's URL for which the changes should be reinstated @@ -312,7 +387,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))); + return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && (isNotEmpty(objectEntry.fieldUpdates) || objectEntry.customOrder.changed))); } /** @@ -330,4 +405,19 @@ 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/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/request.effects.ts b/src/app/core/data/request.effects.ts index 1f8f15c019..44d8416f35 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'; @@ -17,11 +16,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); }) ); @@ -45,9 +44,9 @@ export class RequestEffects { } return this.restApi.request(request.method, request.href, body, request.options, request.isMultipart).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) )) ); }) @@ -67,7 +66,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.service.spec.ts b/src/app/core/data/request.service.spec.ts index 017721fdf9..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'; @@ -22,6 +22,7 @@ import { } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; +import { parseJsonSchemaToCommandDescription } from '@angular/cli/utilities/json-schema'; describe('RequestService', () => { let scheduler: TestScheduler; @@ -139,13 +140,21 @@ describe('RequestService', () => { describe('getByUUID', () => { describe('if the request with the specified UUID exists in the store', () => { beforeEach(() => { + let callCounter = 0; + const responses = [ + cold('a', { // A direct hit in the request cache + a: { + completed: true + } + }), + cold('b', { b: undefined }), // No hit in the index + cold('c', { c: undefined }) // So no mapped hit in the request cache + ]; selectSpy.and.callFake(() => { return () => { - return () => hot('a', { - a: { - completed: true - } - }); + const response = responses[callCounter]; + callCounter++; + return () => response; }; }); }); @@ -162,11 +171,19 @@ describe('RequestService', () => { }); }); - describe('if the request with the specified UUID doesn\'t exist in the store', () => { + describe(`if the request with the specified UUID doesn't exist in the store `, () => { beforeEach(() => { + let callCounter = 0; + const responses = [ + cold('a', { a: undefined }), // No direct hit in the request cache + cold('b', { b: undefined }), // No hit in the index + cold('c', { c: undefined }), // So no mapped hit in the request cache + ]; selectSpy.and.callFake(() => { return () => { - return () => hot('a', { a: undefined }); + const response = responses[callCounter]; + callCounter++; + return () => response; }; }); }); @@ -174,11 +191,43 @@ describe('RequestService', () => { it('should return an Observable of undefined', () => { const result = service.getByUUID(testUUID); - scheduler.expectObservable(result).toBe('b', { b: undefined }); + scheduler.expectObservable(result).toBe('a', { a: undefined }); }); }); - }); + describe(`if the request with the specified UUID wasn't sent, because it was already cached`, () => { + beforeEach(() => { + let callCounter = 0; + const responses = [ + cold('a', { a: undefined }), // No direct hit in the request cache with that UUID + cold('b', { b: 'otherRequestUUID' }), // A hit in the index, which returns the uuid of the cached request + cold('c', { // the call to retrieve the cached request using the UUID from the index + c: { + completed: true + } + }) + ]; + selectSpy.and.callFake(() => { + return () => { + const response = responses[callCounter]; + callCounter++; + return () => response; + }; + }); + }); + + it(`it should return the cached request`, () => { + const result = service.getByUUID(testUUID); + + scheduler.expectObservable(result).toBe('c', { + c: { + completed: true + } + }); + }); + }); + + }); describe('getByHref', () => { describe('when the request with the specified href exists in the store', () => { diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 810b0721ae..105d84cf4a 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -2,10 +2,10 @@ import { Injectable } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { Observable, race as observableRace } from 'rxjs'; -import { filter, map, mergeMap, take } from 'rxjs/operators'; +import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; +import { filter, map, mergeMap, take, switchMap, startWith } from 'rxjs/operators'; import { cloneDeep, remove } from 'lodash'; -import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, hasValueOperator } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; @@ -110,15 +110,19 @@ export class RequestService { * Retrieve a RequestEntry based on their uuid */ getByUUID(uuid: string): Observable { - return observableRace( - this.store.pipe(select(entryFromUUIDSelector(uuid))), + return observableCombineLatest([ + this.store.pipe( + select(entryFromUUIDSelector(uuid)) + ), this.store.pipe( select(originalRequestUUIDFromRequestUUIDSelector(uuid)), - mergeMap((originalUUID) => { - return this.store.pipe(select(entryFromUUIDSelector(originalUUID))) + switchMap((originalUUID) => { + return this.store.pipe(select(entryFromUUIDSelector(originalUUID))) }, - )) - ).pipe( + ), + ), + ]).pipe( + map((entries: RequestEntry[]) => entries.find((entry: RequestEntry) => hasValue(entry))), map((entry: RequestEntry) => { // Headers break after being retrieved from the store (because of lazy initialization) // Combining them with a new object fixes this issue @@ -137,7 +141,13 @@ export class RequestService { getByHref(href: string): Observable { return this.store.pipe( select(uuidFromHrefSelector(href)), - mergeMap((uuid: string) => this.getByUUID(uuid)) + mergeMap((uuid: string) => { + if (isNotEmpty(uuid)) { + return this.getByUUID(uuid); + } else { + return [undefined]; + } + }) ); } 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/data/workflow-action-data.service.ts b/src/app/core/data/workflow-action-data.service.ts new file mode 100644 index 0000000000..be2a170ac5 --- /dev/null +++ b/src/app/core/data/workflow-action-data.service.ts @@ -0,0 +1,41 @@ +import { DataService } from './data.service'; +import { WorkflowAction } from '../tasks/models/workflow-action-object.model'; +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 { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { FindListOptions } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { Injectable } from '@angular/core'; +import { dataService } from '../cache/builders/build-decorators'; +import { WORKFLOW_ACTION } from '../tasks/models/workflow-action-object.resource-type'; + +/** + * A service responsible for fetching/sending data from/to the REST API on the workflowactions endpoint + */ +@Injectable() +@dataService(WORKFLOW_ACTION) +export class WorkflowActionDataService extends DataService { + protected linkPath = 'workflowactions'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } +} diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index 1831386321..d83a376da9 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -8,18 +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 { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; import { SearchParam } from '../cache/models/search-param.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { PaginatedList } from '../data/paginated-list'; @@ -32,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; @@ -39,43 +35,15 @@ describe('EPersonDataService', () => { let requestService: RequestService; let scheduler: TestScheduler; - const epeople = [EPersonMock, EPersonMock2]; + let epeople; - const restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; - const epersonsEndpoint = `${restEndpointURL}/epersons`; - let halService: any = new HALEndpointServiceStub(restEndpointURL); - const epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople])); - const rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ }); - const objectCache = Object.assign({ - /* tslint:disable:no-empty */ - remove: () => { - }, - hasBySelfLinkObservable: () => observableOf(false) - /* tslint:enable:no-empty */ - }) as ObjectCacheService; + let restEndpointURL; + let epersonsEndpoint; + let halService: any; + let epeople$; + let rdbService; - TestBed.configureTestingModule({ - imports: [ - CommonModule, - StoreModule.forRoot({}), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: MockTranslateLoader - } - }), - ], - declarations: [], - providers: [], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); - - const getRequestEntry$ = (successful: boolean) => { - return observableOf({ - completed: true, - response: { isSuccessful: successful, payload: epeople } as any - } as RequestEntry) - }; + let getRequestEntry$; function initTestService() { return new EPersonDataService( @@ -90,7 +58,39 @@ describe('EPersonDataService', () => { ); } + function init() { + getRequestEntry$ = (successful: boolean) => { + return observableOf({ + completed: true, + response: { isSuccessful: successful, payload: epeople } as any + } as RequestEntry) + }; + restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; + epersonsEndpoint = `${restEndpointURL}/epersons`; + epeople = [EPersonMock, EPersonMock2]; + epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople])); + rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ }); + halService = new HALEndpointServiceStub(restEndpointURL); + + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({}), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + } + beforeEach(() => { + init(); requestService = getMockRequestService(getRequestEntry$(true)); store = new Store(undefined, undefined, undefined); service = initTestService(); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index ec8b96d1cd..a8cee6f1de 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -181,7 +181,7 @@ export class EPersonDataService extends DataService { } /** - * Method that clears a cached EPerson request and returns its REST url + * Method that clears a cached EPerson request */ public clearEPersonRequests(): void { this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { @@ -189,6 +189,13 @@ export class EPersonDataService extends DataService { }); } + /** + * Method that clears a link's requests in cache + */ + public clearLinkRequests(href: string): void { + this.requestService.removeByHrefSubstring(href); + } + /** * Method to retrieve the eperson that is currently being edited */ @@ -219,4 +226,27 @@ export class EPersonDataService extends DataService { return this.delete(ePerson.id); } + /** + * Change which ePerson is being edited and return the link for EPeople edit page + * @param ePerson New EPerson to edit + */ + public startEditingNewEPerson(ePerson: EPerson): string { + this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { + if (ePerson === activeEPerson) { + this.cancelEditEPerson(); + } else { + this.editEPerson(ePerson); + } + }); + return '/admin/access-control/epeople'; + } + + /** + * Get EPeople admin page + * @param ePerson New EPerson to edit + */ + public getEPeoplePageRouterLink(): string { + return '/admin/access-control/epeople'; + } + } 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 new file mode 100644 index 0000000000..28d10cfcf1 --- /dev/null +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -0,0 +1,198 @@ +import { CommonModule } from '@angular/common'; +import { HttpHeaders } from '@angular/common/http'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Store, StoreModule } from '@ngrx/store'; +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { compare, Operation } from 'fast-json-patch'; +import { + GroupRegistryCancelGroupAction, + GroupRegistryEditGroupAction +} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; +import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; +import { SearchParam } from '../cache/models/search-param.model'; +import { CoreState } from '../core.reducers'; +import { ChangeAnalyzer } from '../data/change-analyzer'; +import { PaginatedList } from '../data/paginated-list'; +import { DeleteByIDRequest, DeleteRequest, FindListOptions, PostRequest } from '../data/request.models'; +import { RequestEntry } from '../data/request.reducer'; +import { RequestService } from '../data/request.service'; +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; + let store: Store; + let requestService: RequestService; + + let restEndpointURL; + let groupsEndpoint; + let groups; + let groups$; + let halService; + let rdbService; + + let getRequestEntry$; + + function init() { + getRequestEntry$ = (successful: boolean) => { + return observableOf({ + completed: true, + response: { isSuccessful: successful, payload: groups } as any + } as RequestEntry) + }; + restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; + groupsEndpoint = `${restEndpointURL}/groups`; + groups = [GroupMock, GroupMock2]; + groups$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), groups)); + rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ }); + halService = new HALEndpointServiceStub(restEndpointURL); + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({}), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + } + + function initTestService() { + return new GroupDataService( + new DummyChangeAnalyzer() as any, + null, + null, + requestService, + rdbService, + store, + null, + halService, + null, + ); + }; + + beforeEach(() => { + init(); + requestService = getMockRequestService(getRequestEntry$(true)); + store = new Store(undefined, undefined, undefined); + service = initTestService(); + spyOn(store, 'dispatch'); + }); + + describe('searchGroups', () => { + beforeEach(() => { + spyOn(service, 'searchBy'); + }); + + it('search with empty query', () => { + service.searchGroups(''); + const options = Object.assign(new FindListOptions(), { + searchParams: [Object.assign(new SearchParam('query', ''))] + }); + expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); + }); + + it('search with query', () => { + service.searchGroups('test'); + const options = Object.assign(new FindListOptions(), { + searchParams: [Object.assign(new SearchParam('query', 'test'))] + }); + expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); + }); + }); + + describe('deleteGroup', () => { + beforeEach(() => { + service.deleteGroup(GroupMock2).subscribe(); + }); + + it('should send DeleteRequest', () => { + const expected = new DeleteByIDRequest(requestService.generateRequestId(), groupsEndpoint + '/' + GroupMock2.uuid, GroupMock2.uuid); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('addSubGroupToGroup', () => { + beforeEach(() => { + service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe(); + }); + it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => { + let headers = new HttpHeaders(); + const options: HttpOptions = Object.create({}); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('deleteSubGroupFromGroup', () => { + beforeEach(() => { + service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe(); + }); + it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('addMemberToGroup', () => { + beforeEach(() => { + service.addMemberToGroup(GroupMock, EPersonMock2).subscribe(); + }); + it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => { + let headers = new HttpHeaders(); + const options: HttpOptions = Object.create({}); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('deleteMemberFromGroup', () => { + beforeEach(() => { + service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe(); + }); + it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('editGroup', () => { + it('should dispatch a EDIT_GROUP action with the groupp to start editing', () => { + service.editGroup(GroupMock); + expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock)); + }); + }); + + describe('cancelEditGroup', () => { + it('should dispatch a CANCEL_EDIT_GROUP action', () => { + service.cancelEditGroup(); + expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryCancelGroupAction()); + }); + }); +}); + +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: Item, object2: Item): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } +} diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 532f42323a..574b4d997a 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -1,28 +1,52 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Store } from '@ngrx/store'; +import { createSelector, select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { filter, map, take } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { + GroupRegistryCancelGroupAction, + GroupRegistryEditGroupAction +} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; +import { GroupRegistryState } from '../../+admin/admin-access-control/group-registry/group-registry.reducers'; +import { AppState } from '../../app.reducer'; +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 { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; import { DataService } from '../data/data.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { PaginatedList } from '../data/paginated-list'; +import { RemoteData } from '../data/remote-data'; +import { + CreateRequest, + DeleteRequest, + FindListOptions, + FindListRequest, + PostRequest +} from '../data/request.models'; import { RequestService } from '../data/request.service'; -import { FindListOptions } from '../data/request.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { configureRequest, getResponseFromEntry} from '../shared/operators'; +import { EPerson } from './models/eperson.model'; import { Group } from './models/group.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { SearchParam } from '../cache/models/search-param.model'; -import { RemoteData } from '../data/remote-data'; -import { PaginatedList } from '../data/paginated-list'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { dataService } from '../cache/builders/build-decorators'; import { GROUP } from './models/group.resource-type'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { Community } from '../shared/community.model'; +import { Collection } from '../shared/collection.model'; +import { ComcolRole } from '../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; + +const groupRegistryStateSelector = (state: AppState) => state.groupRegistry; +const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup); /** - * Provides methods to retrieve eperson group resources. + * Provides methods to retrieve eperson group resources from the REST API & Group related CRUD actions. */ @Injectable({ providedIn: 'root' @@ -31,6 +55,8 @@ import { GROUP } from './models/group.resource-type'; export class GroupDataService extends DataService { protected linkPath = 'groups'; protected browseEndpoint = ''; + public ePersonsEndpoint = 'epersons'; + public subgroupsEndpoint = 'subgroups'; constructor( protected comparator: DSOChangeAnalyzer, @@ -38,13 +64,52 @@ export class GroupDataService extends DataService { protected notificationsService: NotificationsService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected store: Store, + protected store: Store, protected objectCache: ObjectCacheService, - protected halService: HALEndpointService + protected halService: HALEndpointService, + protected nameService: DSONameService, ) { super(); } + /** + * Retrieves all groups + * @param pagination The pagination info used to retrieve the groups + */ + public getGroups(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + const hrefObs = this.getFindAllHref(options, this.linkPath, ...linksToFollow); + hrefObs.pipe( + filter((href: string) => hasValue(href)), + take(1)) + .subscribe((href: string) => { + const request = new FindListRequest(this.requestService.generateRequestId(), href, options); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs) as Observable>>; + } + + /** + * Returns a search result list of groups, with certain query (searches in group name and by exact uuid) + * Endpoint used: /eperson/groups/search/byMetadata?query=<:name> + * @param query search query param + * @param options + * @param linksToFollow + */ + public searchGroups(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + const searchParams = [new SearchParam('query', query)]; + let findListOptions = new FindListOptions(); + if (options) { + findListOptions = Object.assign(new FindListOptions(), options); + } + if (findListOptions.searchParams) { + findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; + } else { + findListOptions.searchParams = searchParams; + } + return this.searchBy('byMetadata', findListOptions, ...linksToFollow); + } + /** * Check if the current user is member of to the indicated group * @@ -59,10 +124,252 @@ export class GroupDataService extends DataService { options.searchParams = [new SearchParam('groupName', groupName)]; return this.searchBy(searchHref, options).pipe( - filter((groups: RemoteData>) => !groups.isResponsePending), - take(1), - map((groups: RemoteData>) => groups.payload.totalElements > 0) - ); + filter((groups: RemoteData>) => !groups.isResponsePending), + take(1), + map((groups: RemoteData>) => groups.payload.totalElements > 0) + ); } + /** + * Method to delete a group + * @param id The group id to delete + */ + public deleteGroup(group: Group): Observable { + return this.delete(group.id); + } + + /** + * Create or Update a group + * If the group contains an id, it is assumed the eperson already exists and is updated instead + * @param group The group to create or update + */ + public createOrUpdateGroup(group: Group): Observable> { + const isUpdate = hasValue(group.id); + if (isUpdate) { + return this.updateGroup(group); + } else { + return this.create(group, null); + } + } + + /** + * // TODO + * @param {DSpaceObject} ePerson The given object + */ + updateGroup(group: Group): Observable> { + // TODO + return null; + } + + /** + * Adds given subgroup as a subgroup to the given active group + * @param activeGroup Group we want to add subgroup to + * @param subgroup Group we want to add as subgroup to activeGroup + */ + addSubGroupToGroup(activeGroup: Group, subgroup: Group): Observable { + const requestId = this.requestService.generateRequestId(); + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); + this.requestService.configure(postRequest); + + return this.fetchResponse(requestId); + } + + /** + * Deletes a given subgroup from the subgroups of the given active group + * @param activeGroup Group we want to delete subgroup from + * @param subgroup Subgroup we want to delete from activeGroup + */ + deleteSubGroupFromGroup(activeGroup: Group, subgroup: Group): Observable { + const requestId = this.requestService.generateRequestId(); + const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); + this.requestService.configure(deleteRequest); + + return this.fetchResponse(requestId); + } + + /** + * Adds given ePerson as member to given group + * @param activeGroup Group we want to add member to + * @param ePerson EPerson we want to add as member to given activeGroup + */ + addMemberToGroup(activeGroup: Group, ePerson: EPerson): Observable { + const requestId = this.requestService.generateRequestId(); + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); + this.requestService.configure(postRequest); + + return this.fetchResponse(requestId); + } + + /** + * Deletes a given ePerson from the members of the given active group + * @param activeGroup Group we want to delete member from + * @param ePerson EPerson we want to delete from members of given activeGroup + */ + deleteMemberFromGroup(activeGroup: Group, ePerson: EPerson): Observable { + const requestId = this.requestService.generateRequestId(); + const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); + this.requestService.configure(deleteRequest); + + return this.fetchResponse(requestId); + } + + /** + * Gets the restResponse from the requestService + * @param requestId + */ + protected fetchResponse(requestId: string): Observable { + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: RestResponse) => { + return response; + }) + ); + } + + /** + * Method to retrieve the group that is currently being edited + */ + public getActiveGroup(): Observable { + return this.store.pipe(select(editGroupSelector)) + } + + /** + * Method to cancel editing a group, dispatches a cancel group action + */ + public cancelEditGroup() { + this.store.dispatch(new GroupRegistryCancelGroupAction()); + } + + /** + * Method to set the group being edited, dispatches an edit group action + * @param group The group to edit + */ + public editGroup(group: Group) { + this.store.dispatch(new GroupRegistryEditGroupAction(group)); + } + + /** + * Method that clears a cached groups request + */ + public clearGroupsRequests(): void { + this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { + this.requestService.removeByHrefSubstring(link); + }); + } + + /** + * Method that clears a cached get subgroups of certain group request + */ + public clearGroupLinkRequests(href: string): void { + this.requestService.removeByHrefSubstring(href); + } + + public getGroupRegistryRouterLink(): string { + return '/admin/access-control/groups'; + } + + /** + * Change which group is being edited and return the link for the edit page of the new group being edited + * @param newGroup New group to edit + */ + public startEditingNewGroup(newGroup: Group): string { + this.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (newGroup === activeGroup) { + this.cancelEditGroup() + } else { + this.editGroup(newGroup) + } + }); + return this.getGroupEditPageRouterLinkWithID(newGroup.id) + } + + /** + * Get Edit page of group + * @param group Group we want edit page for + */ + public getGroupEditPageRouterLink(group: Group): string { + return this.getGroupEditPageRouterLinkWithID(group.id); + } + + /** + * Get Edit page of group + * @param groupID Group ID we want edit page for + */ + public getGroupEditPageRouterLinkWithID(groupId: string): string { + return '/admin/access-control/groups/' + groupId; + } + + /** + * Extract optional UUID from a string + * @param stringWithUUID String with possible UUID + */ + public getUUIDFromString(stringWithUUID: string): string { + let foundUUID = ''; + const uuidMatches = stringWithUUID.match(/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/g); + if (uuidMatches != null) { + foundUUID = uuidMatches[0]; + } + return foundUUID; + } + + /** + * Create a group for a given role for a given community or collection. + * + * @param dso The community or collection for which to create a group + * @param link The REST endpoint to create the group + */ + createComcolGroup(dso: Community|Collection, link: string): Observable { + + const requestId = this.requestService.generateRequestId(); + const group = Object.assign(new Group(), { + metadata: { + 'dc.description': [ + { + value: `${this.nameService.getName(dso)} admin group`, + } + ], + }, + }); + + this.requestService.configure( + new CreateRequest( + requestId, + link, + JSON.stringify(group), + )); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + tap(() => this.requestService.removeByHrefSubstring(link)), + ); + } + + /** + * Delete the group for a given role for a given community or collection. + * + * @param link The REST endpoint to delete the group + */ + deleteComcolGroup(link: string): Observable { + + const requestId = this.requestService.generateRequestId(); + + this.requestService.configure( + new DeleteRequest( + requestId, + link, + )); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + tap(() => this.requestService.removeByHrefSubstring(link)), + ); + } } diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 5d531800b8..e496babddc 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -6,6 +6,8 @@ import { RemoteData } from '../../data/remote-data'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { HALLink } from '../../shared/hal-link.model'; +import { EPerson } from './eperson.model'; +import { EPERSON } from './eperson.resource-type'; import { GROUP } from './group.resource-type'; @typedObject @@ -13,6 +15,12 @@ import { GROUP } from './group.resource-type'; export class Group extends DSpaceObject { static type = GROUP; + /** + * A string representing the unique name of this Group + */ + @autoserialize + public name: string; + /** * A string representing the unique handle of this Group */ @@ -31,7 +39,8 @@ export class Group extends DSpaceObject { @deserialize _links: { self: HALLink; - groups: HALLink; + subgroups: HALLink; + epersons: HALLink; }; /** @@ -39,6 +48,13 @@ export class Group extends DSpaceObject { * Will be undefined unless the groups {@link HALLink} has been resolved. */ @link(GROUP, true) - public groups?: Observable>>; + public subgroups?: Observable>>; + + /** + * The list of EPeople in this group + * Will be undefined unless the epersons {@link HALLink} has been resolved. + */ + @link(EPERSON, true) + public epersons?: Observable>>; } 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.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index b466693649..d1d817ff36 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -17,12 +17,12 @@ import { MetadataRegistrySelectFieldAction, MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; 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 { StoreMock } from '../../shared/testing/store.mock'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { @@ -40,6 +40,7 @@ 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'; @Component({ template: '' }) class DummyComponent { @@ -151,7 +152,7 @@ describe('RegistryService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CommonModule, StoreModule.forRoot({}), TranslateModule.forRoot()], + imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()], declarations: [ DummyComponent ], @@ -159,7 +160,7 @@ describe('RegistryService', () => { { 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() }, RegistryService ] 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/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 231d44eeff..ab9d1548b7 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -54,7 +54,7 @@ export class Bitstream extends DSpaceObject implements HALResource { * The BitstreamFormat of this Bitstream * Will be undefined unless the format {@link HALLink} has been resolved. */ - @link(BITSTREAM_FORMAT) + @link(BITSTREAM_FORMAT, false, 'format') format?: Observable>; } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index ba2f448bba..4e0b5ead83 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -15,6 +15,8 @@ import { RESOURCE_POLICY } from './resource-policy.resource-type'; import { COMMUNITY } from './community.resource-type'; import { Community } from './community.model'; import { ChildHALResource } from './child-hal-resource.model'; +import { GROUP } from '../eperson/models/group.resource-type'; +import { Group } from '../eperson/models/group.model'; @typedObject @inheritSerialization(DSpaceObject) @@ -70,6 +72,12 @@ export class Collection extends DSpaceObject implements ChildHALResource { @link(COMMUNITY, false) parentCommunity?: Observable>; + /** + * The administrators group of this community. + */ + @link(GROUP) + adminGroup?: Observable>; + /** * The introductory text of this Collection * Corresponds to the metadata field dc.description diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index e18ec743e8..bdcda70e9b 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -3,6 +3,8 @@ import { Observable } from 'rxjs'; import { link, typedObject } from '../cache/builders/build-decorators'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; +import { Group } from '../eperson/models/group.model'; +import { GROUP } from '../eperson/models/group.resource-type'; import { Bitstream } from './bitstream.model'; import { BITSTREAM } from './bitstream.resource-type'; import { Collection } from './collection.model'; @@ -32,6 +34,7 @@ export class Community extends DSpaceObject implements ChildHALResource { logo: HALLink; subcommunities: HALLink; parentCommunity: HALLink; + adminGroup: HALLink; self: HALLink; }; @@ -63,6 +66,12 @@ export class Community extends DSpaceObject implements ChildHALResource { @link(COMMUNITY, false) parentCommunity?: Observable>; + /** + * The administrators group of this community. + */ + @link(GROUP) + adminGroup?: Observable>; + /** * The introductory text of this Community * Corresponds to the metadata field dc.description diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 60a1160d3e..a9256fbb7f 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,5 +1,5 @@ import { autoserialize, autoserializeAs, deserialize, deserializeAs } from 'cerialize'; -import { hasNoValue, isUndefined } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isUndefined } from '../../shared/empty.util'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { typedObject } from '../cache/builders/build-decorators'; import { CacheableObject } from '../cache/object-cache.reducer'; @@ -79,6 +79,9 @@ export class DSpaceObject extends ListableObject implements CacheableObject { * The name for this DSpaceObject */ set name(name) { + if (hasValue(this.firstMetadata('dc.title'))) { + this.firstMetadata('dc.title').value = name; + } this._name = name; } 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/metadata.utils.spec.ts b/src/app/core/shared/metadata.utils.spec.ts index f4b3517649..016ef594b1 100644 --- a/src/app/core/shared/metadata.utils.spec.ts +++ b/src/app/core/shared/metadata.utils.spec.ts @@ -7,6 +7,7 @@ import { MetadatumViewModel } from './metadata.models'; import { Metadata } from './metadata.utils'; +import { beforeEach } from 'selenium-webdriver/testing'; const mdValue = (value: string, language?: string, authority?: string): MetadataValue => { return Object.assign(new MetadataValue(), { uuid: uuidv4(), value: value, language: isUndefined(language) ? null : language, place: 0, authority: isUndefined(authority) ? null : authority, confidence: undefined }); @@ -216,4 +217,26 @@ describe('Metadata', () => { testToMetadataMap(multiViewModelList, multiMap); }); + describe('setFirstValue method', () => { + + const metadataMap = { + 'dc.description': [mdValue('Test description')], + 'dc.title': [mdValue('Test title 1'), mdValue('Test title 2')] + }; + + const testSetFirstValue = (map: MetadataMap, key: string, value: string) => { + describe(`with field ${key} and value ${value}`, () => { + Metadata.setFirstValue(map, key, value); + it(`should set first value of ${key} to ${value}`, () => { + expect(map[key][0].value).toEqual(value); + }); + }); + }; + + testSetFirstValue(metadataMap, 'dc.description', 'New Description'); + testSetFirstValue(metadataMap, 'dc.title', 'New Title'); + testSetFirstValue(metadataMap, 'dc.format', 'Completely new field and value'); + + }); + }); diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts index 334c430968..24ff06f4c9 100644 --- a/src/app/core/shared/metadata.utils.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -1,4 +1,4 @@ -import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util'; import { MetadataMapInterface, MetadataValue, @@ -217,4 +217,19 @@ export class Metadata { }); return metadataMap; } + + /** + * Set the first value of a metadata by field key + * Creates a new MetadataValue if the field doesn't exist yet + * @param mdMap The map to add/change values in + * @param key The metadata field + * @param value The value to add + */ + public static setFirstValue(mdMap: MetadataMapInterface, key: string, value: string) { + if (isNotEmpty(mdMap[key])) { + mdMap[key][0].value = value; + } else { + mdMap[key] = [Object.assign(new MetadataValue(), { value: value })] + } + } } 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 14d101a448..a51e711d26 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,6 +1,6 @@ import { Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { filter, find, flatMap, map, tap } from 'rxjs/operators'; +import { filter, find, flatMap, map, take, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { SearchResult } from '../../shared/search/search-result.model'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; @@ -207,3 +207,13 @@ export const getFirstOccurrence = () => source.pipe( map((rd) => Object.assign(rd, { payload: rd.payload.page.length > 0 ? rd.payload.page[0] : undefined })) ); + +/** + * Operator for turning the current page of bitstreams into an array + */ +export const paginatedListToArray = () => + (source: Observable>>): Observable => + source.pipe( + hasValueOperator(), + map((objectRD: RemoteData>) => objectRD.payload.page.filter((object: T) => hasValue(object))) + ); 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/tasks/claimed-task-data.service.spec.ts b/src/app/core/tasks/claimed-task-data.service.spec.ts index 90d449b22b..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'; @@ -52,8 +52,7 @@ describe('ClaimedTaskDataService', () => { options.headers = headers; }); - describe('approveTask', () => { - + describe('submitTask', () => { it('should call postToEndpoint method', () => { const scopeId = '1234'; const body = { @@ -63,33 +62,13 @@ describe('ClaimedTaskDataService', () => { spyOn(service, 'postToEndpoint'); requestService.uriEncodeBody.and.returnValue(body); - service.approveTask(scopeId); - - expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, body, scopeId, options); - }); - }); - - describe('rejectTask', () => { - - it('should call postToEndpoint method', () => { - const scopeId = '1234'; - const reason = 'test reject'; - const body = { - submit_reject: 'true', - reason - }; - - spyOn(service, 'postToEndpoint'); - requestService.uriEncodeBody.and.returnValue(body); - - service.rejectTask(reason, scopeId); + service.submitTask(scopeId, body); expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, body, scopeId, options); }); }); describe('returnToPoolTask', () => { - it('should call deleteById method', () => { const scopeId = '1234'; diff --git a/src/app/core/tasks/claimed-task-data.service.ts b/src/app/core/tasks/claimed-task-data.service.ts index 0a9de20530..5815dad6e5 100644 --- a/src/app/core/tasks/claimed-task-data.service.ts +++ b/src/app/core/tasks/claimed-task-data.service.ts @@ -35,7 +35,6 @@ export class ClaimedTaskDataService extends TasksService { * * @param {RequestService} requestService * @param {RemoteDataBuildService} rdbService - * @param {NormalizedObjectBuildService} linkService * @param {Store} store * @param {ObjectCacheService} objectCache * @param {HALEndpointService} halService @@ -56,35 +55,16 @@ export class ClaimedTaskDataService extends TasksService { } /** - * Make a request to approve the given task + * Make a request for the given task * * @param scopeId * The task id + * @param body + * The request body * @return {Observable} * Emit the server response */ - public approveTask(scopeId: string): Observable { - const body = { - submit_approve: 'true' - }; - return this.postToEndpoint(this.linkPath, this.requestService.uriEncodeBody(body), scopeId, this.makeHttpOptions()); - } - - /** - * Make a request to reject the given task - * - * @param reason - * The reason of reject - * @param scopeId - * The task id - * @return {Observable} - * Emit the server response - */ - public rejectTask(reason: string, scopeId: string): Observable { - const body = { - submit_reject: 'true', - reason - }; + public submitTask(scopeId: string, body: any): Observable { return this.postToEndpoint(this.linkPath, this.requestService.uriEncodeBody(body), scopeId, this.makeHttpOptions()); } diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index b56cec3a7e..d90c7a19c0 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -13,6 +13,8 @@ import { HALLink } from '../../shared/hal-link.model'; import { WorkflowItem } from '../../submission/models/workflowitem.model'; import { TASK_OBJECT } from './task-object.resource-type'; import { WORKFLOWITEM } from '../../eperson/models/workflowitem.resource-type'; +import { WORKFLOW_ACTION } from './workflow-action-object.resource-type'; +import { WorkflowAction } from './workflow-action-object.model'; /** * An abstract model class for a TaskObject. @@ -34,12 +36,6 @@ export class TaskObject extends DSpaceObject implements CacheableObject { @autoserialize step: string; - /** - * The task action type - */ - @autoserialize - action: string; - /** * The {@link HALLink}s for this TaskObject */ @@ -49,6 +45,7 @@ export class TaskObject extends DSpaceObject implements CacheableObject { owner: HALLink; group: HALLink; workflowitem: HALLink; + action: HALLink; }; /** @@ -70,6 +67,14 @@ 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 + * Will be undefined unless the group {@link HALLink} has been resolved. + */ + @link(WORKFLOW_ACTION, false, 'action') + action: Observable>; } diff --git a/src/app/core/tasks/models/workflow-action-object.model.ts b/src/app/core/tasks/models/workflow-action-object.model.ts new file mode 100644 index 0000000000..720d817859 --- /dev/null +++ b/src/app/core/tasks/models/workflow-action-object.model.ts @@ -0,0 +1,25 @@ +import { inheritSerialization, autoserialize } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { WORKFLOW_ACTION } from './workflow-action-object.resource-type'; + +/** + * A model class for a WorkflowAction + */ +@typedObject +@inheritSerialization(DSpaceObject) +export class WorkflowAction extends DSpaceObject { + static type = WORKFLOW_ACTION; + + /** + * The workflow action's identifier + */ + @autoserialize + id: string; + + /** + * The options available for this workflow action + */ + @autoserialize + options: string[]; +} diff --git a/src/app/core/tasks/models/workflow-action-object.resource-type.ts b/src/app/core/tasks/models/workflow-action-object.resource-type.ts new file mode 100644 index 0000000000..d48ffd18f4 --- /dev/null +++ b/src/app/core/tasks/models/workflow-action-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for WorkflowAction + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const WORKFLOW_ACTION = new ResourceType('workflowaction'); 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.spec.ts b/src/app/header/header.component.spec.ts index c46eef75e2..1c3133375a 100644 --- a/src/app/header/header.component.spec.ts +++ b/src/app/header/header.component.spec.ts @@ -9,7 +9,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { MenuService } from '../shared/menu/menu.service'; -import { MenuServiceStub } from '../shared/testing/menu-service-stub'; +import { MenuServiceStub } from '../shared/testing/menu-service.stub'; let comp: HeaderComponent; let fixture: ComponentFixture; diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts index d1061c72bc..7aec26c140 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts @@ -2,12 +2,12 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component'; import { By } from '@angular/platform-browser'; -import { MenuServiceStub } from '../../shared/testing/menu-service-stub'; +import { MenuServiceStub } from '../../shared/testing/menu-service.stub'; import { Component } from '@angular/core'; import { of as observableOf } from 'rxjs'; import { HostWindowService } from '../../shared/host-window.service'; import { MenuService } from '../../shared/menu/menu.service'; -import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('ExpandableNavbarSectionComponent', () => { diff --git a/src/app/navbar/navbar-section/navbar-section.component.spec.ts b/src/app/navbar/navbar-section/navbar-section.component.spec.ts index af46227ec3..90df574021 100644 --- a/src/app/navbar/navbar-section/navbar-section.component.spec.ts +++ b/src/app/navbar/navbar-section/navbar-section.component.spec.ts @@ -4,9 +4,9 @@ import { NavbarSectionComponent } from './navbar-section.component'; import { HostWindowService } from '../../shared/host-window.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MenuService } from '../../shared/menu/menu.service'; -import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { Component } from '@angular/core'; -import { MenuServiceStub } from '../../shared/testing/menu-service-stub'; +import { MenuServiceStub } from '../../shared/testing/menu-service.stub'; import { of as observableOf } from 'rxjs'; describe('NavbarSectionComponent', () => { diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts index ca054a662b..206afda003 100644 --- a/src/app/navbar/navbar.component.spec.ts +++ b/src/app/navbar/navbar.component.spec.ts @@ -6,12 +6,11 @@ import { of as observableOf } from 'rxjs'; import { NavbarComponent } from './navbar.component'; import { ReactiveFormsModule } from '@angular/forms'; 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 { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Injector, NO_ERRORS_SCHEMA } from '@angular/core'; import { MenuService } from '../shared/menu/menu.service'; -import { MenuServiceStub } from '../shared/testing/menu-service-stub'; -import { ENV_CONFIG, GLOBAL_CONFIG } from '../../config'; +import { MenuServiceStub } from '../shared/testing/menu-service.stub'; let comp: NavbarComponent; let fixture: ComponentFixture; @@ -31,7 +30,6 @@ describe('NavbarComponent', () => { { provide: Injector, useValue: {} }, { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG } ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index b2ba10fb98..6055aac263 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -6,7 +6,7 @@ import { MenuID, MenuItemType } from '../shared/menu/initial-menus-state'; import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { HostWindowService } from '../shared/host-window.service'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../config'; +import { environment } from '../../environments/environment'; /** * Component representing the public navbar @@ -24,8 +24,7 @@ export class NavbarComponent extends MenuComponent implements OnInit { */ menuID = MenuID.PUBLIC; - constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig, - protected menuService: MenuService, + constructor(protected menuService: MenuService, protected injector: Injector, public windowService: HostWindowService ) { @@ -80,7 +79,7 @@ export class NavbarComponent extends MenuComponent implements OnInit { }, ]; // Read the different Browse-By types from config and add them to the browse menu - const types = this.config.browseBy.types; + const types = environment.browseBy.types; types.forEach((typeConfig) => { menuList.push({ id: `browse_global_by_${typeConfig.id}`, diff --git a/src/app/navbar/navbar.effects.spec.ts b/src/app/navbar/navbar.effects.spec.ts index 897fc15be7..34e22ad59d 100644 --- a/src/app/navbar/navbar.effects.spec.ts +++ b/src/app/navbar/navbar.effects.spec.ts @@ -8,7 +8,7 @@ import * as fromRouter from '@ngrx/router-store'; import { CollapseMenuAction } from '../shared/menu/menu.actions'; import { MenuID } from '../shared/menu/initial-menus-state'; import { MenuService } from '../shared/menu/menu.service'; -import { MenuServiceStub } from '../shared/testing/menu-service-stub'; +import { MenuServiceStub } from '../shared/testing/menu-service.stub'; describe('NavbarEffects', () => { let navbarEffects: NavbarEffects; diff --git a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts index 7aeb33d84d..f331750b26 100644 --- a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts +++ b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts @@ -5,72 +5,68 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { EPerson } from '../../core/eperson/models/eperson.model'; -import { GLOBAL_CONFIG } from '../../../config'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { cloneDeep } from 'lodash'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; describe('ProfilePageMetadataFormComponent', () => { let component: ProfilePageMetadataFormComponent; let fixture: ComponentFixture; - const config = { - languages: [{ - code: 'en', - label: 'English', - active: true, - }, { - code: 'de', - label: 'Deutsch', - active: true, - }] - } as any; + let user; - const user = Object.assign(new EPerson(), { - email: 'example@gmail.com', - metadata: { - 'eperson.firstname': [ - { - value: 'John', - language: null - } - ], - 'eperson.lastname': [ - { - value: 'Doe', - language: null - } - ], - 'eperson.language': [ - { - value: 'de', - language: null - } - ] - } - }); + let epersonService; + let notificationsService; + let translate; - const epersonService = jasmine.createSpyObj('epersonService', { - update: createSuccessfulRemoteDataObject$(user) - }); - const notificationsService = jasmine.createSpyObj('notificationsService', { - success: {}, - error: {}, - warning: {} - }); - const translate = { - instant: () => 'translated', - onLangChange: new EventEmitter() - }; + function init() { + user = Object.assign(new EPerson(), { + email: 'example@gmail.com', + metadata: { + 'eperson.firstname': [ + { + value: 'John', + language: null + } + ], + 'eperson.lastname': [ + { + value: 'Doe', + language: null + } + ], + 'eperson.language': [ + { + value: 'de', + language: null + } + ] + } + }); + + epersonService = jasmine.createSpyObj('epersonService', { + update: createSuccessfulRemoteDataObject$(user) + }); + notificationsService = jasmine.createSpyObj('notificationsService', { + success: {}, + error: {}, + warning: {} + }); + translate = { + instant: () => 'translated', + onLangChange: new EventEmitter() + }; + + } beforeEach(async(() => { + init(); TestBed.configureTestingModule({ declarations: [ProfilePageMetadataFormComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ - { provide: GLOBAL_CONFIG, useValue: config }, { provide: EPersonDataService, useValue: epersonService }, { provide: TranslateService, useValue: translate }, { provide: NotificationsService, useValue: notificationsService }, diff --git a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts index b44faa8c4a..c1216cbb5f 100644 --- a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts +++ b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts @@ -1,20 +1,20 @@ import { Component, Inject, Input, OnInit } from '@angular/core'; import { DynamicFormControlModel, - DynamicFormService, DynamicFormValueControlModel, + DynamicFormValueControlModel, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { FormGroup } from '@angular/forms'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { TranslateService } from '@ngx-translate/core'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { LangConfig } from '../../../config/lang-config.interface'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { cloneDeep } from 'lodash'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../core/shared/operators'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { environment } from '../../../environments/environment'; @Component({ selector: 'ds-profile-page-metadata-form', @@ -102,15 +102,14 @@ export class ProfilePageMetadataFormComponent implements OnInit { */ activeLangs: LangConfig[]; - constructor(@Inject(GLOBAL_CONFIG) protected config: GlobalConfig, - protected formBuilderService: FormBuilderService, + constructor(protected formBuilderService: FormBuilderService, protected translate: TranslateService, protected epersonService: EPersonDataService, protected notificationsService: NotificationsService) { } ngOnInit(): void { - this.activeLangs = this.config.languages.filter((MyLangConfig) => MyLangConfig.active === true); + this.activeLangs = environment.languages.filter((MyLangConfig) => MyLangConfig.active === true); this.setFormValues(); this.updateFieldTranslations(); this.translate.onLangChange diff --git a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts index 324230ce9f..225bd8507e 100644 --- a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts +++ b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts @@ -15,22 +15,30 @@ describe('ProfilePageSecurityFormComponent', () => { let component: ProfilePageSecurityFormComponent; let fixture: ComponentFixture; - const user = Object.assign(new EPerson(), { - _links: { - self: { href: 'user-selflink' } - } - }); + let user; - const epersonService = jasmine.createSpyObj('epersonService', { - patch: observableOf(new RestResponse(true, 200, 'OK')) - }); - const notificationsService = jasmine.createSpyObj('notificationsService', { - success: {}, - error: {}, - warning: {} - }); + let epersonService; + let notificationsService; + + function init() { + user = Object.assign(new EPerson(), { + _links: { + self: { href: 'user-selflink' } + } + }); + + epersonService = jasmine.createSpyObj('epersonService', { + patch: observableOf(new RestResponse(true, 200, 'OK')) + }); + notificationsService = jasmine.createSpyObj('notificationsService', { + success: {}, + error: {}, + warning: {} + }); + } beforeEach(async(() => { + init(); TestBed.configureTestingModule({ declarations: [ProfilePageSecurityFormComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], diff --git a/src/app/profile-page/profile-page.component.spec.ts b/src/app/profile-page/profile-page.component.spec.ts index 5992012be9..d4a65c0ba0 100644 --- a/src/app/profile-page/profile-page.component.spec.ts +++ b/src/app/profile-page/profile-page.component.spec.ts @@ -5,39 +5,48 @@ import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { EPerson } from '../core/eperson/models/eperson.model'; -import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../shared/testing/utils'; import { Store, StoreModule } from '@ngrx/store'; import { AppState } from '../app.reducer'; import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { authReducer } from '../core/auth/auth.reducer'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { createPaginatedList } from '../shared/testing/utils.test'; describe('ProfilePageComponent', () => { let component: ProfilePageComponent; let fixture: ComponentFixture; + let user; + let authState; - const user = Object.assign(new EPerson(), { - groups: createSuccessfulRemoteDataObject$(createPaginatedList([])) - }); - const authState = { - authenticated: true, - loaded: true, - loading: false, - authToken: new AuthTokenInfo('test_token'), - user: user - }; + let epersonService; + let notificationsService; - const epersonService = jasmine.createSpyObj('epersonService', { - findById: createSuccessfulRemoteDataObject$(user) - }); - const notificationsService = jasmine.createSpyObj('notificationsService', { - success: {}, - error: {}, - warning: {} - }); + function init() { + user = Object.assign(new EPerson(), { + groups: createSuccessfulRemoteDataObject$(createPaginatedList([])) + }); + authState = { + authenticated: true, + loaded: true, + loading: false, + authToken: new AuthTokenInfo('test_token'), + user: user + }; + + epersonService = jasmine.createSpyObj('epersonService', { + findById: createSuccessfulRemoteDataObject$(user) + }); + notificationsService = jasmine.createSpyObj('notificationsService', { + success: {}, + error: {}, + warning: {} + }); + } beforeEach(async(() => { + init(); TestBed.configureTestingModule({ declarations: [ProfilePageComponent, VarDirective], imports: [StoreModule.forRoot(authReducer), TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], diff --git a/src/app/search-navbar/search-navbar.component.spec.ts b/src/app/search-navbar/search-navbar.component.spec.ts index 2a03acd2a2..1388b23113 100644 --- a/src/app/search-navbar/search-navbar.component.spec.ts +++ b/src/app/search-navbar/search-navbar.component.spec.ts @@ -5,7 +5,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { SearchService } from '../core/shared/search/search.service'; -import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { SearchNavbarComponent } from './search-navbar.component'; @@ -34,7 +34,7 @@ describe('SearchNavbarComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } })], declarations: [SearchNavbarComponent], diff --git a/src/app/shared/alert/alert.component.spec.ts b/src/app/shared/alert/alert.component.spec.ts index e235e27b28..51e584c3fa 100644 --- a/src/app/shared/alert/alert.component.spec.ts +++ b/src/app/shared/alert/alert.component.spec.ts @@ -7,7 +7,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { AlertComponent } from './alert.component'; -import { createTestComponent } from '../testing/utils'; +import { createTestComponent } from '../testing/utils.test'; import { AlertType } from './aletr-type'; describe('AlertComponent test suite', () => { diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index 5e01494674..a05afbdbfb 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -5,11 +5,11 @@ import { By } from '@angular/platform-browser'; import { Store, StoreModule } from '@ngrx/store'; import { authReducer, AuthState } from '../../core/auth/auth.reducer'; -import { EPersonMock } from '../testing/eperson-mock'; +import { EPersonMock } from '../testing/eperson.mock'; import { TranslateModule } from '@ngx-translate/core'; -import { AppState } from '../../app.reducer'; +import { AppState, storeModuleConfig } from '../../app.reducer'; import { AuthNavMenuComponent } from './auth-nav-menu.component'; -import { HostWindowServiceStub } from '../testing/host-window-service-stub'; +import { HostWindowServiceStub } from '../testing/host-window-service.stub'; import { HostWindowService } from '../host-window.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; @@ -51,7 +51,12 @@ describe('AuthNavMenuComponent', () => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, - StoreModule.forRoot(authReducer), + StoreModule.forRoot(authReducer, { + runtimeChecks: { + strictStateImmutability: false, + strictActionImmutability: false + } + }), TranslateModule.forRoot() ], declarations: [ @@ -244,7 +249,12 @@ describe('AuthNavMenuComponent', () => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, - StoreModule.forRoot(authReducer), + StoreModule.forRoot(authReducer, { + runtimeChecks: { + strictStateImmutability: false, + strictActionImmutability: false + } + }), TranslateModule.forRoot() ], declarations: [ diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts index 512d9e0917..06a25e7ff8 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts @@ -7,9 +7,9 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { UserMenuComponent } from './user-menu.component'; import { authReducer, AuthState } from '../../../core/auth/auth.reducer'; import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model'; -import { EPersonMock } from '../../testing/eperson-mock'; -import { AppState } from '../../../app.reducer'; -import { MockTranslateLoader } from '../../mocks/mock-translate-loader'; +import { EPersonMock } from '../../testing/eperson.mock'; +import { AppState, storeModuleConfig } from '../../../app.reducer'; +import { TranslateLoaderMock } from '../../mocks/translate-loader.mock'; import { cold } from 'jasmine-marbles'; import { By } from '@angular/platform-browser'; @@ -41,11 +41,16 @@ describe('UserMenuComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot(authReducer), + StoreModule.forRoot(authReducer, { + runtimeChecks: { + strictStateImmutability: false, + strictActionImmutability: false + } + }), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } }) ], diff --git a/src/app/shared/authority-confidence/authority-confidence-state.directive.ts b/src/app/shared/authority-confidence/authority-confidence-state.directive.ts index 6362daf3c7..410dadfc5f 100644 --- a/src/app/shared/authority-confidence/authority-confidence-state.directive.ts +++ b/src/app/shared/authority-confidence/authority-confidence-state.directive.ts @@ -1,8 +1,9 @@ import { + AfterViewInit, Directive, - ElementRef, EventEmitter, + ElementRef, + EventEmitter, HostListener, - Inject, Input, OnChanges, Output, @@ -16,8 +17,8 @@ import { AuthorityValue } from '../../core/integration/models/authority.value'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { ConfidenceType } from '../../core/integration/models/confidence-type'; import { isNotEmpty, isNull } from '../empty.util'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { ConfidenceIconConfig } from '../../../config/submission-config.interface'; +import { environment } from '../../../environments/environment'; /** * Directive to add to the element a bootstrap utility class based on metadata confidence value @@ -25,7 +26,7 @@ import { ConfidenceIconConfig } from '../../../config/submission-config.interfac @Directive({ selector: '[dsAuthorityConfidenceState]' }) -export class AuthorityConfidenceStateDirective implements OnChanges { +export class AuthorityConfidenceStateDirective implements OnChanges, AfterViewInit { /** * The metadata value @@ -69,7 +70,6 @@ export class AuthorityConfidenceStateDirective implements OnChanges { * @param {Renderer2} renderer */ constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private elem: ElementRef, private renderer: Renderer2 ) { @@ -135,7 +135,7 @@ export class AuthorityConfidenceStateDirective implements OnChanges { return 'd-none'; } - const confidenceIcons: ConfidenceIconConfig[] = this.EnvConfig.submission.icons.authority.confidence; + const confidenceIcons: ConfidenceIconConfig[] = environment.submission.icons.authority.confidence; const confidenceIndex: number = findIndex(confidenceIcons, {value: confidence}); diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index 51db888c4b..b21fd2968d 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -12,12 +12,13 @@ import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { StoreModule } from '@ngrx/store'; -import { MockTranslateLoader } from '../mocks/mock-translate-loader'; +import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { createSuccessfulRemoteDataObject$ } from '../testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { storeModuleConfig } from '../../app.reducer'; describe('BrowseByComponent', () => { let comp: BrowseByComponent; @@ -52,11 +53,11 @@ describe('BrowseByComponent', () => { TranslateModule.forRoot(), SharedModule, NgbModule, - StoreModule.forRoot({}), + StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } }), RouterTestingModule, diff --git a/src/app/shared/chips/chips.component.html b/src/app/shared/chips/chips.component.html index db8f08dad0..74006c80ca 100644 --- a/src/app/shared/chips/chips.component.html +++ b/src/app/shared/chips/chips.component.html @@ -33,7 +33,7 @@ [authorityValue]="c.item[icon.metadata] || c.item" [visibleWhenAuthorityEmpty]="icon.visibleWhenAuthorityEmpty" aria-hidden="true" - (dragstart)="tooltip.close();" + (dragstart)="t.close();" (mouseover)="showTooltip(t, i, icon.metadata)" (mouseout)="t.close()"> diff --git a/src/app/shared/chips/chips.component.spec.ts b/src/app/shared/chips/chips.component.spec.ts index facfc8061b..accbaf8f34 100644 --- a/src/app/shared/chips/chips.component.spec.ts +++ b/src/app/shared/chips/chips.component.spec.ts @@ -8,14 +8,12 @@ import { ChipsComponent } from './chips.component'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { By } from '@angular/platform-browser'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; -import { createTestComponent } from '../testing/utils'; +import { createTestComponent } from '../testing/utils.test'; import { AuthorityConfidenceStateDirective } from '../authority-confidence/authority-confidence-state.directive'; import { TranslateModule } from '@ngx-translate/core'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; -import { MOCK_SUBMISSION_CONFIG } from '../testing/mock-submission-config'; import { ConfidenceType } from '../../core/integration/models/confidence-type'; import { SortablejsModule } from 'ngx-sortablejs'; +import { environment } from '../../../environments/environment'; describe('ChipsComponent test suite', () => { @@ -26,7 +24,6 @@ describe('ChipsComponent test suite', () => { let html; let chips: Chips; - const envConfig: GlobalConfig = MOCK_SUBMISSION_CONFIG; // async beforeEach beforeEach(async(() => { @@ -42,7 +39,6 @@ describe('ChipsComponent test suite', () => { AuthorityConfidenceStateDirective ], // declare the test component providers: [ - { provide: GLOBAL_CONFIG, useValue: envConfig }, ChangeDetectorRef, ChipsComponent, UploaderService @@ -154,7 +150,7 @@ describe('ChipsComponent test suite', () => { otherRelatedField: new FormFieldMetadataValueObject('other related test') }; - chips = new Chips([item], 'display', 'mainField', envConfig.submission.icons.metadata); + chips = new Chips([item], 'display', 'mainField', environment.submission.icons.metadata); chipsFixture = TestBed.createComponent(ChipsComponent); chipsComp = chipsFixture.componentInstance; // TruncatableComponent test instance chipsComp.editable = true; @@ -177,7 +173,7 @@ describe('ChipsComponent test suite', () => { icons[0].triggerEventHandler('mouseover', null); - expect(chipsComp.tipText).toEqual(['main test']) + expect(chipsComp.tipText).toEqual(['main test']); }); }); }); diff --git a/src/app/shared/chips/models/chips-item.model.ts b/src/app/shared/chips/models/chips-item.model.ts index 913232fa71..d60d29e41e 100644 --- a/src/app/shared/chips/models/chips-item.model.ts +++ b/src/app/shared/chips/models/chips-item.model.ts @@ -102,7 +102,7 @@ export class ChipsItem { const obj = this.objToDisplay ? this._item[this.objToDisplay] : this._item; if (isObject(obj) && obj) { - value = obj[this.fieldToDisplay] || obj.value; + value = obj[this.fieldToDisplay] || (obj as any).value; } else { value = obj; } @@ -113,6 +113,6 @@ export class ChipsItem { private hasPlaceholder(value: any) { return (typeof value === 'string') ? (value === PLACEHOLDER_PARENT_METADATA) : - (value as FormFieldMetadataValueObject).hasPlaceholder() + (value as FormFieldMetadataValueObject).hasPlaceholder(); } } diff --git a/src/app/shared/chips/models/chips.model.ts b/src/app/shared/chips/models/chips.model.ts index 25754361cb..de694bdcfd 100644 --- a/src/app/shared/chips/models/chips.model.ts +++ b/src/app/shared/chips/models/chips.model.ts @@ -74,7 +74,7 @@ export class Chips { private hasPlaceholder(value) { if (isObject(value)) { - return value.value === PLACEHOLDER_PARENT_METADATA; + return (value as any).value === PLACEHOLDER_PARENT_METADATA; } else { return value === PLACEHOLDER_PARENT_METADATA; } 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 06f9843c6d..a1bac46f87 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,9 +17,9 @@ 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'; @@ -137,7 +137,7 @@ describe('ComColFormComponent', () => { type: Community.type }, ), - uploader: {} as any, + uploader: undefined, deleteLogo: false } ); @@ -318,7 +318,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 35c6f50969..f8199d2aad 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 @@ -39,7 +39,7 @@ export class ComColFormComponent implements OnInit, OnDe /** * The logo uploader component */ - @ViewChild(UploaderComponent, {static: true}) uploaderComponent: UploaderComponent; + @ViewChild(UploaderComponent, {static: false}) uploaderComponent: UploaderComponent; /** * DSpaceObject that the form represents 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/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..414d64cbff 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,8 +12,8 @@ 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 { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { ComcolMetadataComponent } from './comcol-metadata.component'; describe('ComColMetadataComponent', () => { diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html new file mode 100644 index 0000000000..1e89e3facf --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html @@ -0,0 +1,52 @@ +
+ +
+ +
+ +
+ {{'comcol-role.edit.' + comcolRole.name + '.name' | translate}} +
+ +
+ +
+ {{'comcol-role.edit.no-group' | translate}} +
+
+ {{'comcol-role.edit.' + comcolRole.name + '.anonymous-group' | translate}} +
+ + {{group.name}} + +
+ +
+
+ {{'comcol-role.edit.create' | translate}} +
+
+ {{'comcol-role.edit.restrict' | translate}} +
+
+ {{'comcol-role.edit.delete' | translate}} +
+
+ +
+ +
+ {{'comcol-role.edit.' + comcolRole.name + '.description' | translate}} +
+ +
+ +
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.scss b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts new file mode 100644 index 0000000000..4694c13603 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts @@ -0,0 +1,176 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComcolRoleComponent } from './comcol-role.component'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { By } from '@angular/platform-browser'; +import { SharedModule } from '../../../shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { RequestService } from '../../../../core/data/request.service'; +import { ComcolRole } from './comcol-role'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Collection } from '../../../../core/shared/collection.model'; + +describe('ComcolRoleComponent', () => { + + let fixture: ComponentFixture; + let comp: ComcolRoleComponent; + let de: DebugElement; + + let requestService; + let groupService; + + let group; + let statusCode; + + beforeEach(() => { + + requestService = {hasByHrefObservable: () => observableOf(true)}; + + groupService = { + findByHref: () => undefined, + createComcolGroup: jasmine.createSpy('createComcolGroup'), + deleteComcolGroup: jasmine.createSpy('deleteComcolGroup'), + }; + + spyOn(groupService, 'findByHref').and.callFake((link) => { + if (link === 'test role link') { + return observableOf(new RemoteData( + false, + false, + true, + undefined, + group, + statusCode, + )); + } + }); + + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterTestingModule.withRoutes([]), + TranslateModule.forRoot(), + ], + providers: [ + { provide: GroupDataService, useValue: groupService }, + { provide: RequestService, useValue: requestService }, + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + + fixture = TestBed.createComponent(ComcolRoleComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + + comp.comcolRole = new ComcolRole( + 'test role name', + 'test role endpoint', + ); + + comp.dso = Object.assign( + new Collection(), { + _links: { + 'test role endpoint': { + href: 'test role link', + } + } + } + ); + + fixture.detectChanges(); + }); + + describe('when there is no group yet', () => { + + beforeEach(() => { + group = null; + statusCode = 204; + fixture.detectChanges(); + }); + + it('should have a create button but no restrict or delete button', () => { + expect(de.query(By.css('.btn.create'))) + .toBeTruthy(); + expect(de.query(By.css('.btn.restrict'))) + .toBeNull(); + expect(de.query(By.css('.btn.delete'))) + .toBeNull(); + }); + + describe('when the create button is pressed', () => { + + beforeEach(() => { + de.query(By.css('.btn.create')).nativeElement.click(); + }); + + it('should call the groupService create method', () => { + expect(groupService.createComcolGroup).toHaveBeenCalled(); + }); + }); + }); + + describe('when the related group is the Anonymous group', () => { + + beforeEach(() => { + group = { + name: 'Anonymous' + }; + statusCode = 200; + fixture.detectChanges(); + }); + + it('should have a restrict button but no create or delete button', () => { + expect(de.query(By.css('.btn.create'))) + .toBeNull(); + expect(de.query(By.css('.btn.restrict'))) + .toBeTruthy(); + expect(de.query(By.css('.btn.delete'))) + .toBeNull(); + }); + + describe('when the restrict button is pressed', () => { + + beforeEach(() => { + de.query(By.css('.btn.restrict')).nativeElement.click(); + }); + + it('should call the groupService create method', () => { + expect(groupService.createComcolGroup).toHaveBeenCalledWith(comp.dso, 'test role link'); + }); + }); + }); + + describe('when the related group is a custom group', () => { + + beforeEach(() => { + group = { + name: 'custom group name' + }; + statusCode = 200; + fixture.detectChanges(); + }); + + it('should have a delete button but no create or restrict button', () => { + expect(de.query(By.css('.btn.create'))) + .toBeNull(); + expect(de.query(By.css('.btn.restrict'))) + .toBeNull(); + expect(de.query(By.css('.btn.delete'))) + .toBeTruthy(); + }); + + describe('when the delete button is pressed', () => { + + beforeEach(() => { + de.query(By.css('.btn.delete')).nativeElement.click(); + }); + + it('should call the groupService delete method', () => { + expect(groupService.deleteComcolGroup).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts new file mode 100644 index 0000000000..41cb7e7cd2 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts @@ -0,0 +1,126 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Group } from '../../../../core/eperson/models/group.model'; +import { Community } from '../../../../core/shared/community.model'; +import { Observable } from 'rxjs'; +import { getGroupEditPath } from '../../../../+admin/admin-access-control/admin-access-control-routing.module'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { Collection } from '../../../../core/shared/collection.model'; +import { filter, map } from 'rxjs/operators'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { ComcolRole } from './comcol-role'; +import { RequestService } from '../../../../core/data/request.service'; +import { RemoteData } from '../../../../core/data/remote-data'; + +/** + * Component for managing a community or collection role. + */ +@Component({ + selector: 'ds-comcol-role', + styleUrls: ['./comcol-role.component.scss'], + templateUrl: './comcol-role.component.html' +}) +export class ComcolRoleComponent implements OnInit { + + /** + * The community or collection to manage. + */ + @Input() + dso: Community|Collection; + + /** + * The role to manage + */ + @Input() + comcolRole: ComcolRole; + + constructor( + protected requestService: RequestService, + protected groupService: GroupDataService, + ) { + } + + /** + * The link to the related group. + */ + get groupLink(): string { + return this.dso._links[this.comcolRole.linkName].href; + } + + /** + * The group for this role, as an observable remote data. + */ + get groupRD$(): Observable> { + return this.groupService.findByHref(this.groupLink).pipe( + filter((groupRD) => !!groupRD.statusCode), + ); + } + + /** + * The group for this role, as an observable. + */ + get group$(): Observable { + return this.groupRD$.pipe( + getSucceededRemoteData(), + filter((groupRD) => groupRD != null), + getRemoteDataPayload(), + ); + } + + /** + * The link to the group edit page as an observable. + */ + get editGroupLink$(): Observable { + return this.group$.pipe( + map((group) => getGroupEditPath(group.id)), + ); + } + + /** + * Return true if there is no group for this ComcolRole, as an observable. + */ + hasNoGroup$(): Observable { + return this.groupRD$.pipe( + map((groupRD) => groupRD.statusCode === 204), + ) + } + + /** + * Return true if the group for this ComcolRole is the Anonymous group, as an observable. + */ + hasAnonymousGroup$(): Observable { + return this.group$.pipe( + map((group) => group.name === 'Anonymous'), + ) + } + + /** + * Return true if there is a group for this ComcolRole other than the Anonymous group, as an observable. + */ + hasCustomGroup$(): Observable { + return this.hasAnonymousGroup$().pipe( + map((anonymous) => !anonymous), + ) + } + + /** + * Create a group for this community or collection role. + */ + create() { + this.groupService.createComcolGroup(this.dso, this.groupLink).subscribe(); + } + + /** + * Delete the group for this community or collection role. + */ + delete() { + this.groupService.deleteComcolGroup(this.groupLink).subscribe(); + } + + ngOnInit(): void { + this.requestService.hasByHrefObservable(this.groupLink) + .pipe( + filter((hasByHrefObservable) => !hasByHrefObservable), + ) + .subscribe(() => this.groupRD$.subscribe()); + } +} diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts new file mode 100644 index 0000000000..2ac74fe67b --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts @@ -0,0 +1,77 @@ +import { Community } from '../../../../core/shared/community.model'; +import { Collection } from '../../../../core/shared/collection.model'; + +/** + * Class representing a community or collection role. + */ +export class ComcolRole { + + /** + * The community admin role. + */ + public static COMMUNITY_ADMIN = new ComcolRole( + 'community-admin', + 'adminGroup', + ); + + /** + * The collection admin role. + */ + public static COLLECTION_ADMIN = new ComcolRole( + 'collection-admin', + 'adminGroup', + ); + + /** + * The submitters role. + */ + public static SUBMITTERS = new ComcolRole( + 'submitters', + 'submittersGroup', + ); + + /** + * The default item read role. + */ + public static ITEM_READ = new ComcolRole( + 'item_read', + 'itemReadGroup', + ); + + /** + * The default bitstream read role. + */ + public static BITSTREAM_READ = new ComcolRole( + 'bitstream_read', + 'bitstreamReadGroup', + ); + + /** + * @param name The name for this community or collection role. + * @param linkName The path linking to this community or collection role. + */ + constructor( + public name, + public linkName, + ) { + } + + /** + * Get the REST endpoint for managing this role for a given community or collection. + * @param dso + */ + public getEndpoint(dso: Community | Collection) { + + let linkPath; + switch (dso.type + '') { + case 'community': + linkPath = 'communities'; + break; + case 'collection': + linkPath = 'collections'; + break; + } + + return `${linkPath}/${dso.uuid}/${this.linkName}`; + } +} 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/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..480f6ff709 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; diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts index 4871d74b98..b723d3fe98 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-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 communityRouter from '../../../../+community-page/community-page-routing.module'; import { Community } from '../../../../core/shared/community.model'; import { CreateCommunityParentSelectorComponent } from './create-community-parent-selector.component'; import { MetadataValue } from '../../../../core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('CreateCommunityParentSelectorComponent', () => { let component: CreateCommunityParentSelectorComponent; diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts index b3058ab879..854349a47c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts @@ -5,11 +5,11 @@ 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 { Collection } from '../../../../core/shared/collection.model'; import { CreateItemParentSelectorComponent } from './create-item-parent-selector.component'; import { MetadataValue } from '../../../../core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('CreateItemParentSelectorComponent', () => { let component: CreateItemParentSelectorComponent; diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index 15f23d1fe6..7b5c020f1b 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -14,7 +14,7 @@ import { By } from '@angular/platform-browser'; import { DSOSelectorComponent } from '../dso-selector/dso-selector.component'; import { MockComponent } from 'ng-mocks'; import { MetadataValue } from '../../../core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject } from '../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../remote-data.utils'; describe('DSOSelectorModalWrapperComponent', () => { let component: DSOSelectorModalWrapperComponent; diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts index cbb8fb654e..a17d9e4c21 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-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 { EditCollectionSelectorComponent } from './edit-collection-selector.component'; import { Collection } from '../../../../core/shared/collection.model'; import { MetadataValue } from '../../../../core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('EditCollectionSelectorComponent', () => { let component: EditCollectionSelectorComponent; diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts index 46684e6cfb..c48d29baa9 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-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 communityRouter from '../../../../+community-page/community-page-routing.module'; import { EditCommunitySelectorComponent } from './edit-community-selector.component'; import { Community } from '../../../../core/shared/community.model'; import { MetadataValue } from '../../../../core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('EditCommunitySelectorComponent', () => { let component: EditCommunitySelectorComponent; diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts index 86066916a6..582320acae 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts @@ -7,10 +7,10 @@ import { ActivatedRoute, Router } from '@angular/router'; import { EditItemSelectorComponent } from './edit-item-selector.component'; import { Item } from '../../../../core/shared/item.model'; import { RemoteData } from '../../../../core/data/remote-data'; -import { RouterStub } from '../../../testing/router-stub'; +import { RouterStub } from '../../../testing/router.stub'; import * as itemRouter from '../../../../+item-page/item-page-routing.module'; import { MetadataValue } from '../../../../core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('EditItemSelectorComponent', () => { let component: EditItemSelectorComponent; diff --git a/src/app/shared/error/error.component.spec.ts b/src/app/shared/error/error.component.spec.ts index 7335f93aed..18ee2f24ef 100644 --- a/src/app/shared/error/error.component.spec.ts +++ b/src/app/shared/error/error.component.spec.ts @@ -4,7 +4,7 @@ import { DebugElement } from '@angular/core'; import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core'; -import { MockTranslateLoader } from '../mocks/mock-translate-loader'; +import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; import { ErrorComponent } from './error.component'; @@ -21,7 +21,7 @@ describe('ErrorComponent (inline template)', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } }), ], diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 75ea88735f..4dee6905d2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -70,7 +70,7 @@ import { SubmissionObjectDataService } from '../../../../core/submission/submiss import { Item } from '../../../../core/shared/item.model'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('DsDynamicFormControlContainerComponent test suite', () => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 4d26f3948d..2089ce8bca 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -76,6 +76,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; +import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model'; +import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component'; import { map, startWith, switchMap, find } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { SearchResult } from '../../../search/search-result.model'; @@ -158,6 +160,9 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< case DYNAMIC_FORM_CONTROL_TYPE_DISABLED: return DsDynamicDisabledComponent; + case DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH: + return CustomSwitchComponent; + default: return null; } @@ -293,6 +298,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } } + get isCheckbox(): boolean { + return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH; + } + ngOnChanges(changes: SimpleChanges) { if (changes) { super.ngOnChanges(changes); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts index fa13febcd1..79a650b597 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -7,7 +7,7 @@ import { select, Store } from '@ngrx/store'; import { Item } from '../../../../../core/shared/item.model'; import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../testing/utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index 9d56d7d1b3..75c27b6ca5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -12,7 +12,7 @@ + + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts new file mode 100644 index 0000000000..6c2502a92b --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts @@ -0,0 +1,99 @@ +import { DynamicFormsCoreModule, DynamicFormService } from '@ng-dynamic-forms/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { DebugElement } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TextMaskModule } from 'angular2-text-mask'; +import { By } from '@angular/platform-browser'; +import { DynamicCustomSwitchModel } from './custom-switch.model'; +import { CustomSwitchComponent } from './custom-switch.component'; + +describe('CustomSwitchComponent', () => { + + const testModel = new DynamicCustomSwitchModel({id: 'switch'}); + const formModel = [testModel]; + let formGroup: FormGroup; + let fixture: ComponentFixture; + let component: CustomSwitchComponent; + let debugElement: DebugElement; + let testElement: DebugElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + NoopAnimationsModule, + TextMaskModule, + DynamicFormsCoreModule.forRoot() + ], + declarations: [CustomSwitchComponent] + + }).compileComponents().then(() => { + fixture = TestBed.createComponent(CustomSwitchComponent); + + component = fixture.componentInstance; + debugElement = fixture.debugElement; + }); + })); + + beforeEach(inject([DynamicFormService], (service: DynamicFormService) => { + formGroup = service.createFormGroup(formModel); + + component.group = formGroup; + component.model = testModel; + + fixture.detectChanges(); + + testElement = debugElement.query(By.css(`input[id='${testModel.id}']`)); + })); + + it('should initialize correctly', () => { + expect(component.bindId).toBe(true); + expect(component.group instanceof FormGroup).toBe(true); + expect(component.model instanceof DynamicCustomSwitchModel).toBe(true); + + expect(component.blur).toBeDefined(); + expect(component.change).toBeDefined(); + expect(component.focus).toBeDefined(); + + expect(component.onBlur).toBeDefined(); + expect(component.onChange).toBeDefined(); + expect(component.onFocus).toBeDefined(); + + expect(component.hasFocus).toBe(false); + expect(component.isValid).toBe(true); + expect(component.isInvalid).toBe(false); + }); + + it('should have an input element', () => { + expect(testElement instanceof DebugElement).toBe(true); + }); + + it('should have an input element of type checkbox', () => { + expect(testElement.nativeElement.getAttribute('type')).toEqual('checkbox'); + }); + + it('should emit blur event', () => { + spyOn(component.blur, 'emit'); + + component.onBlur(null); + + expect(component.blur.emit).toHaveBeenCalled(); + }); + + it('should emit change event', () => { + spyOn(component.change, 'emit'); + + component.onChange(null); + + expect(component.change.emit).toHaveBeenCalled(); + }); + + it('should emit focus event', () => { + spyOn(component.focus, 'emit'); + + component.onFocus(null); + + expect(component.focus.emit).toHaveBeenCalled(); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts new file mode 100644 index 0000000000..ab02fc159d --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts @@ -0,0 +1,55 @@ +import { DynamicNGBootstrapCheckboxComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { DynamicCustomSwitchModel } from './custom-switch.model'; + +@Component({ + selector: 'ds-custom-switch', + styleUrls: ['./custom-switch.component.scss'], + templateUrl: './custom-switch.component.html', +}) +/** + * Component displaying a custom switch usable in dynamic forms + * Extends from bootstrap's checkbox component but displays a switch instead + */ +export class CustomSwitchComponent extends DynamicNGBootstrapCheckboxComponent { + /** + * Use the model's ID for the input element + */ + @Input() bindId = true; + + /** + * The formgroup containing this component + */ + @Input() group: FormGroup; + + /** + * The model used for displaying the switch + */ + @Input() model: DynamicCustomSwitchModel; + + /** + * Emit an event when the input is selected + */ + @Output() selected = new EventEmitter(); + + /** + * Emit an event when the input value is removed + */ + @Output() remove = new EventEmitter(); + + /** + * Emit an event when the input is blurred out + */ + @Output() blur = new EventEmitter(); + + /** + * Emit an event when the input value changes + */ + @Output() change = new EventEmitter(); + + /** + * Emit an event when the input is focused + */ + @Output() focus = new EventEmitter(); +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts new file mode 100644 index 0000000000..97cf71c4a0 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts @@ -0,0 +1,20 @@ +import { + DynamicCheckboxModel, + DynamicCheckboxModelConfig, + DynamicFormControlLayout, + serializable +} from '@ng-dynamic-forms/core'; + +export const DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH = 'CUSTOM_SWITCH'; + +/** + * Model class for displaying a custom switch input in a form + * Functions like a checkbox, but displays a switch instead + */ +export class DynamicCustomSwitchModel extends DynamicCheckboxModel { + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH; + + constructor(config: DynamicCheckboxModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts index ad54925880..78c2d5d217 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts @@ -12,7 +12,7 @@ import { FormBuilderService } from '../../../form-builder.service'; import { FormComponent } from '../../../../form.component'; import { FormService } from '../../../../form.service'; -import { createTestComponent } from '../../../../../testing/utils'; +import { createTestComponent } from '../../../../../testing/utils.test'; export const DATE_TEST_GROUP = new FormGroup({ date: new FormControl() diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html index 6fede2eff0..897ea4c5e3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html @@ -7,7 +7,6 @@ = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html index 3cfb5980c6..254b15411d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -59,7 +59,7 @@ ngbTooltip="{{'form.clear-help' | translate}}" placement="top" [disabled]="model.readOnly" - (click)="remove($event)">{{'form.clear' | translate}} + (click)="remove()">{{'form.clear' | translate}}