diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index ba882b50b8..4702bbe354 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -14,7 +14,7 @@ import { } from './+search-page/search-filters/search-filter/search-filter.reducer'; import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; -import { itemSelectionReducer, ItemSelectionsState } from './shared/item-select/item-select.reducer'; +import { objectSelectionReducer, ObjectSelectionsState } from './shared/object-select/object-select.reducer'; export interface AppState { router: fromRouter.RouterReducerState; @@ -25,7 +25,7 @@ export interface AppState { searchSidebar: SearchSidebarState; searchFilter: SearchFiltersState; truncatable: TruncatablesState, - itemSelection: ItemSelectionsState + objectSelection: ObjectSelectionsState } export const appReducers: ActionReducerMap = { @@ -37,7 +37,7 @@ export const appReducers: ActionReducerMap = { searchSidebar: sidebarReducer, searchFilter: filterReducer, truncatable: truncatableReducer, - itemSelection: itemSelectionReducer + objectSelection: objectSelectionReducer }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 4ea3aa8df7..256d58b5b8 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -64,8 +64,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { UploaderService } from '../shared/uploader/uploader.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; -import { ItemSelectService } from '../shared/item-select/item-select.service'; import { MappingCollectionsReponseParsingService } from './data/mapping-collections-reponse-parsing.service'; +import { ObjectSelectService } from '../shared/object-select/object-select.service'; const IMPORTS = [ CommonModule, @@ -131,7 +131,7 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, - ItemSelectService, + ObjectSelectService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/shared/item-select/item-select.actions.ts b/src/app/shared/item-select/item-select.actions.ts deleted file mode 100644 index 0f17575a28..0000000000 --- a/src/app/shared/item-select/item-select.actions.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { type } from '../ngrx/type'; -import { Action } from '@ngrx/store'; - -export const ItemSelectionActionTypes = { - INITIAL_DESELECT: type('dspace/item-select/INITIAL_DESELECT'), - INITIAL_SELECT: type('dspace/item-select/INITIAL_SELECT'), - SELECT: type('dspace/item-select/SELECT'), - DESELECT: type('dspace/item-select/DESELECT'), - SWITCH: type('dspace/item-select/SWITCH'), - RESET: type('dspace/item-select/RESET') -}; - -export class ItemSelectionAction implements Action { - /** - * UUID of the item a select action can be performed on - */ - id: string; - - /** - * Type of action that will be performed - */ - type; - - /** - * Initialize with the item's UUID - * @param {string} id of the item - */ - constructor(id: string) { - this.id = id; - } -} - -/* tslint:disable:max-classes-per-file */ -/** - * Used to set the initial state to deselected - */ -export class ItemSelectionInitialDeselectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.INITIAL_DESELECT; -} - -/** - * Used to set the initial state to selected - */ -export class ItemSelectionInitialSelectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.INITIAL_SELECT; -} - -/** - * Used to select an item - */ -export class ItemSelectionSelectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.SELECT; -} - -/** - * Used to deselect an item - */ -export class ItemSelectionDeselectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.DESELECT; -} - -/** - * Used to switch an item between selected and deselected - */ -export class ItemSelectionSwitchAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.SWITCH; -} - -/** - * Used to reset all item's selected to be deselected - */ -export class ItemSelectionResetAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.RESET; -} -/* tslint:enable:max-classes-per-file */ diff --git a/src/app/shared/item-select/item-select.reducer.spec.ts b/src/app/shared/item-select/item-select.reducer.spec.ts deleted file mode 100644 index 10c5db9b7f..0000000000 --- a/src/app/shared/item-select/item-select.reducer.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ItemSelectionDeselectAction, ItemSelectionInitialDeselectAction, - ItemSelectionInitialSelectAction, ItemSelectionResetAction, - ItemSelectionSelectAction, ItemSelectionSwitchAction -} from './item-select.actions'; -import { itemSelectionReducer } from './item-select.reducer'; - -const itemId1 = 'id1'; -const itemId2 = 'id2'; - -class NullAction extends ItemSelectionSelectAction { - type = null; - - constructor() { - super(undefined); - } -} - -describe('itemSelectionReducer', () => { - - it('should return the current state when no valid actions have been made', () => { - const state = {}; - state[itemId1] = { checked: true }; - const action = new NullAction(); - const newState = itemSelectionReducer(state, action); - - expect(newState).toEqual(state); - }); - - it('should start with an empty object', () => { - const state = {}; - const action = new NullAction(); - const newState = itemSelectionReducer(undefined, action); - - expect(newState).toEqual(state); - }); - - it('should set checked to true in response to the INITIAL_SELECT action', () => { - const action = new ItemSelectionInitialSelectAction(itemId1); - const newState = itemSelectionReducer(undefined, action); - - expect(newState[itemId1].checked).toBeTruthy(); - }); - - it('should set checked to true in response to the INITIAL_DESELECT action', () => { - const action = new ItemSelectionInitialDeselectAction(itemId1); - const newState = itemSelectionReducer(undefined, action); - - expect(newState[itemId1].checked).toBeFalsy(); - }); - - it('should set checked to true in response to the SELECT action', () => { - const state = {}; - state[itemId1] = { checked: false }; - const action = new ItemSelectionSelectAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeTruthy(); - }); - - it('should set checked to false in response to the DESELECT action', () => { - const state = {}; - state[itemId1] = { checked: true }; - const action = new ItemSelectionDeselectAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeFalsy(); - }); - - it('should set checked from false to true in response to the SWITCH action', () => { - const state = {}; - state[itemId1] = { checked: false }; - const action = new ItemSelectionSwitchAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeTruthy(); - }); - - it('should set checked from true to false in response to the SWITCH action', () => { - const state = {}; - state[itemId1] = { checked: true }; - const action = new ItemSelectionSwitchAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeFalsy(); - }); - - it('should set reset the state in response to the RESET action', () => { - const state = {}; - state[itemId1] = { checked: true }; - state[itemId2] = { checked: false }; - const action = new ItemSelectionResetAction(undefined); - const newState = itemSelectionReducer(state, action); - - expect(newState).toEqual({}); - }); - -}); diff --git a/src/app/shared/item-select/item-select.service.spec.ts b/src/app/shared/item-select/item-select.service.spec.ts deleted file mode 100644 index f7b28a5b04..0000000000 --- a/src/app/shared/item-select/item-select.service.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ItemSelectService } from './item-select.service'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; -import { ItemSelectionsState } from './item-select.reducer'; -import { AppState } from '../../app.reducer'; -import { - ItemSelectionDeselectAction, - ItemSelectionInitialDeselectAction, - ItemSelectionInitialSelectAction, ItemSelectionResetAction, - ItemSelectionSelectAction, ItemSelectionSwitchAction -} from './item-select.actions'; - -describe('ItemSelectService', () => { - let service: ItemSelectService; - - const mockItemId = 'id1'; - - const store: Store = jasmine.createSpyObj('store', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: Observable.of(true) - }); - - const appStore: Store = jasmine.createSpyObj('appStore', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: Observable.of(true) - }); - - beforeEach(() => { - service = new ItemSelectService(store, appStore); - }); - - describe('when the initialSelect method is triggered', () => { - beforeEach(() => { - service.initialSelect(mockItemId); - }); - - it('ItemSelectionInitialSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionInitialSelectAction(mockItemId)); - }); - }); - - describe('when the initialDeselect method is triggered', () => { - beforeEach(() => { - service.initialDeselect(mockItemId); - }); - - it('ItemSelectionInitialDeselectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionInitialDeselectAction(mockItemId)); - }); - }); - - describe('when the select method is triggered', () => { - beforeEach(() => { - service.select(mockItemId); - }); - - it('ItemSelectionSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionSelectAction(mockItemId)); - }); - }); - - describe('when the deselect method is triggered', () => { - beforeEach(() => { - service.deselect(mockItemId); - }); - - it('ItemSelectionDeselectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionDeselectAction(mockItemId)); - }); - }); - - describe('when the switch method is triggered', () => { - beforeEach(() => { - service.switch(mockItemId); - }); - - it('ItemSelectionSwitchAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionSwitchAction(mockItemId)); - }); - }); - - describe('when the reset method is triggered', () => { - beforeEach(() => { - service.reset(); - }); - - it('ItemSelectionInitialSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionResetAction(null)); - }); - }); - -}); diff --git a/src/app/shared/item-select/item-select.service.ts b/src/app/shared/item-select/item-select.service.ts deleted file mode 100644 index bf4c7ba239..0000000000 --- a/src/app/shared/item-select/item-select.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Injectable } from '@angular/core'; -import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; -import { ItemSelectionsState, ItemSelectionState } from './item-select.reducer'; -import { - ItemSelectionDeselectAction, - ItemSelectionInitialDeselectAction, - ItemSelectionInitialSelectAction, ItemSelectionResetAction, - ItemSelectionSelectAction, ItemSelectionSwitchAction -} from './item-select.actions'; -import { Observable } from 'rxjs/Observable'; -import { hasValue } from '../empty.util'; -import { map } from 'rxjs/operators'; -import { AppState } from '../../app.reducer'; - -const selectionStateSelector = (state: ItemSelectionsState) => state.itemSelection; -const itemSelectionsStateSelector = (state: AppState) => state.itemSelection; - -/** - * Service that takes care of selecting and deselecting items - */ -@Injectable() -export class ItemSelectService { - - constructor( - private store: Store, - private appStore: Store - ) { - } - - /** - * Request the current selection of a given item - * @param {string} id The UUID of the item - * @returns {Observable} Emits the current selection state of the given item, if it's unavailable, return false - */ - getSelected(id: string): Observable { - return this.store.select(selectionByIdSelector(id)).pipe( - map((object: ItemSelectionState) => { - if (object) { - return object.checked; - } else { - return false; - } - }) - ); - } - - /** - * Request the current selection of a given item - * @param {string} id The UUID of the item - * @returns {Observable} Emits the current selection state of the given item, if it's unavailable, return false - */ - getAllSelected(): Observable { - return this.appStore.select(itemSelectionsStateSelector).pipe( - map((state: ItemSelectionsState) => Object.keys(state).filter((key) => state[key].checked)) - ); - } - - /** - * Dispatches an initial select action to the store for a given item - * @param {string} id The UUID of the item to select - */ - public initialSelect(id: string): void { - this.store.dispatch(new ItemSelectionInitialSelectAction(id)); - } - - /** - * Dispatches an initial deselect action to the store for a given item - * @param {string} id The UUID of the item to deselect - */ - public initialDeselect(id: string): void { - this.store.dispatch(new ItemSelectionInitialDeselectAction(id)); - } - - /** - * Dispatches a select action to the store for a given item - * @param {string} id The UUID of the item to select - */ - public select(id: string): void { - this.store.dispatch(new ItemSelectionSelectAction(id)); - } - - /** - * Dispatches a deselect action to the store for a given item - * @param {string} id The UUID of the item to deselect - */ - public deselect(id: string): void { - this.store.dispatch(new ItemSelectionDeselectAction(id)); - } - - /** - * Dispatches a switch action to the store for a given item - * @param {string} id The UUID of the item to select - */ - public switch(id: string): void { - this.store.dispatch(new ItemSelectionSwitchAction(id)); - } - - /** - * Dispatches a reset action to the store for all items - */ - public reset(): void { - this.store.dispatch(new ItemSelectionResetAction(null)); - } - -} - -function selectionByIdSelector(id: string): MemoizedSelector { - return keySelector(id); -} - -export function keySelector(key: string): MemoizedSelector { - return createSelector(selectionStateSelector, (state: ItemSelectionState) => { - if (hasValue(state)) { - return state[key]; - } else { - return undefined; - } - }); -} diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html similarity index 100% rename from src/app/shared/item-select/item-select.component.html rename to src/app/shared/object-select/item-select/item-select.component.html diff --git a/src/app/shared/item-select/item-select.component.scss b/src/app/shared/object-select/item-select/item-select.component.scss similarity index 100% rename from src/app/shared/item-select/item-select.component.scss rename to src/app/shared/object-select/item-select/item-select.component.scss diff --git a/src/app/shared/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts similarity index 76% rename from src/app/shared/item-select/item-select.component.spec.ts rename to src/app/shared/object-select/item-select/item-select.component.spec.ts index 0f3a9d5fae..9708e43ca9 100644 --- a/src/app/shared/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -1,30 +1,25 @@ import { ItemSelectComponent } from './item-select.component'; import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { SharedModule } from '../shared.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { ItemSelectService } from './item-select.service'; -import { ItemSelectServiceStub } from '../testing/item-select-service-stub'; -import { Observable } from 'rxjs/Observable'; -import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { Item } from '../../core/shared/item.model'; -import { By } from '@angular/platform-browser'; -import { ActivatedRoute, Route, Router } from '@angular/router'; -import { ActivatedRouteStub } from '../testing/active-router-stub'; -import { RouterStub } from '../testing/router-stub'; -import { HostWindowService } from '../host-window.service'; -import { HostWindowServiceStub } from '../testing/host-window-service-stub'; -import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { LocationStrategy } from '@angular/common'; -import { MockLocationStrategy } from '@angular/common/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { Item } from '../../../core/shared/item.model'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../shared.module'; +import { ObjectSelectServiceStub } from '../../testing/object-select-service-stub'; +import { ObjectSelectService } from '../object-select.service'; +import { HostWindowService } from '../../host-window.service'; +import { HostWindowServiceStub } from '../../testing/host-window-service-stub'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; let fixture: ComponentFixture; - let itemSelectService: ItemSelectService; + let itemSelectService: ObjectSelectService; const mockItemList = [ Object.assign(new Item(), { @@ -70,7 +65,7 @@ describe('ItemSelectComponent', () => { imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], declarations: [], providers: [ - { provide: ItemSelectService, useValue: new ItemSelectServiceStub() }, + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts similarity index 72% rename from src/app/shared/item-select/item-select.component.ts rename to src/app/shared/object-select/item-select/item-select.component.ts index 3a2003a327..6ada45cb3b 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -1,12 +1,11 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { ItemDataService } from '../../core/data/item-data.service'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { RemoteData } from '../../core/data/remote-data'; -import { Observable } from 'rxjs/Observable'; -import { Item } from '../../core/shared/item.model'; -import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { ItemSelectService } from './item-select.service'; import { take } from 'rxjs/operators'; +import { Observable } from 'rxjs/Observable'; +import { Item } from '../../../core/shared/item.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { ObjectSelectService } from '../object-select.service'; @Component({ selector: 'ds-item-select', @@ -50,11 +49,11 @@ export class ItemSelectComponent implements OnInit { */ selectedIds$: Observable; - constructor(private itemSelectService: ItemSelectService) { + constructor(private objectelectService: ObjectSelectService) { } ngOnInit(): void { - this.selectedIds$ = this.itemSelectService.getAllSelected(); + this.selectedIds$ = this.objectelectService.getAllSelected(); } /** @@ -62,7 +61,7 @@ export class ItemSelectComponent implements OnInit { * @param {string} id */ switch(id: string) { - this.itemSelectService.switch(id); + this.objectelectService.switch(id); } /** @@ -71,7 +70,7 @@ export class ItemSelectComponent implements OnInit { * @returns {Observable} */ getSelected(id: string): Observable { - return this.itemSelectService.getSelected(id); + return this.objectelectService.getSelected(id); } /** @@ -83,7 +82,7 @@ export class ItemSelectComponent implements OnInit { take(1) ).subscribe((ids: string[]) => { this.confirm.emit(ids); - this.itemSelectService.reset(); + this.objectelectService.reset(); }); } diff --git a/src/app/shared/object-select/object-select.actions.ts b/src/app/shared/object-select/object-select.actions.ts new file mode 100644 index 0000000000..4adaeb9fed --- /dev/null +++ b/src/app/shared/object-select/object-select.actions.ts @@ -0,0 +1,75 @@ +import { type } from '../ngrx/type'; +import { Action } from '@ngrx/store'; + +export const ObjectSelectionActionTypes = { + INITIAL_DESELECT: type('dspace/object-select/INITIAL_DESELECT'), + INITIAL_SELECT: type('dspace/object-select/INITIAL_SELECT'), + SELECT: type('dspace/object-select/SELECT'), + DESELECT: type('dspace/object-select/DESELECT'), + SWITCH: type('dspace/object-select/SWITCH'), + RESET: type('dspace/object-select/RESET') +}; + +export class ObjectSelectionAction implements Action { + /** + * UUID of the object a select action can be performed on + */ + id: string; + + /** + * Type of action that will be performed + */ + type; + + /** + * Initialize with the object's UUID + * @param {string} id of the object + */ + constructor(id: string) { + this.id = id; + } +} + +/* tslint:disable:max-classes-per-file */ +/** + * Used to set the initial state to deselected + */ +export class ObjectSelectionInitialDeselectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.INITIAL_DESELECT; +} + +/** + * Used to set the initial state to selected + */ +export class ObjectSelectionInitialSelectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.INITIAL_SELECT; +} + +/** + * Used to select an object + */ +export class ObjectSelectionSelectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.SELECT; +} + +/** + * Used to deselect an object + */ +export class ObjectSelectionDeselectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.DESELECT; +} + +/** + * Used to switch an object between selected and deselected + */ +export class ObjectSelectionSwitchAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.SWITCH; +} + +/** + * Used to reset all objects selected to be deselected + */ +export class ObjectSelectionResetAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.RESET; +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/shared/object-select/object-select.reducer.spec.ts b/src/app/shared/object-select/object-select.reducer.spec.ts new file mode 100644 index 0000000000..696df97d39 --- /dev/null +++ b/src/app/shared/object-select/object-select.reducer.spec.ts @@ -0,0 +1,98 @@ +import { + ObjectSelectionDeselectAction, ObjectSelectionInitialDeselectAction, + ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, + ObjectSelectionSelectAction, ObjectSelectionSwitchAction +} from './object-select.actions'; +import { objectSelectionReducer } from './object-select.reducer'; + +const objectId1 = 'id1'; +const objectId2 = 'id2'; + +class NullAction extends ObjectSelectionSelectAction { + type = null; + + constructor() { + super(undefined); + } +} + +describe('objectSelectionReducer', () => { + + it('should return the current state when no valid actions have been made', () => { + const state = {}; + state[objectId1] = { checked: true }; + const action = new NullAction(); + const newState = objectSelectionReducer(state, action); + + expect(newState).toEqual(state); + }); + + it('should start with an empty object', () => { + const state = {}; + const action = new NullAction(); + const newState = objectSelectionReducer(undefined, action); + + expect(newState).toEqual(state); + }); + + it('should set checked to true in response to the INITIAL_SELECT action', () => { + const action = new ObjectSelectionInitialSelectAction(objectId1); + const newState = objectSelectionReducer(undefined, action); + + expect(newState[objectId1].checked).toBeTruthy(); + }); + + it('should set checked to true in response to the INITIAL_DESELECT action', () => { + const action = new ObjectSelectionInitialDeselectAction(objectId1); + const newState = objectSelectionReducer(undefined, action); + + expect(newState[objectId1].checked).toBeFalsy(); + }); + + it('should set checked to true in response to the SELECT action', () => { + const state = {}; + state[objectId1] = { checked: false }; + const action = new ObjectSelectionSelectAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeTruthy(); + }); + + it('should set checked to false in response to the DESELECT action', () => { + const state = {}; + state[objectId1] = { checked: true }; + const action = new ObjectSelectionDeselectAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeFalsy(); + }); + + it('should set checked from false to true in response to the SWITCH action', () => { + const state = {}; + state[objectId1] = { checked: false }; + const action = new ObjectSelectionSwitchAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeTruthy(); + }); + + it('should set checked from true to false in response to the SWITCH action', () => { + const state = {}; + state[objectId1] = { checked: true }; + const action = new ObjectSelectionSwitchAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeFalsy(); + }); + + it('should set reset the state in response to the RESET action', () => { + const state = {}; + state[objectId1] = { checked: true }; + state[objectId2] = { checked: false }; + const action = new ObjectSelectionResetAction(undefined); + const newState = objectSelectionReducer(state, action); + + expect(newState).toEqual({}); + }); + +}); diff --git a/src/app/shared/item-select/item-select.reducer.ts b/src/app/shared/object-select/object-select.reducer.ts similarity index 67% rename from src/app/shared/item-select/item-select.reducer.ts rename to src/app/shared/object-select/object-select.reducer.ts index 6306adf0c4..bd54e43a35 100644 --- a/src/app/shared/item-select/item-select.reducer.ts +++ b/src/app/shared/object-select/object-select.reducer.ts @@ -1,21 +1,21 @@ import { isEmpty } from '../empty.util'; -import { ItemSelectionAction, ItemSelectionActionTypes } from './item-select.actions'; +import { ObjectSelectionAction, ObjectSelectionActionTypes } from './object-select.actions'; /** * Interface that represents the state for a single filters */ -export interface ItemSelectionState { +export interface ObjectSelectionState { checked: boolean; } /** * Interface that represents the state for all available filters */ -export interface ItemSelectionsState { - [id: string]: ItemSelectionState +export interface ObjectSelectionsState { + [id: string]: ObjectSelectionState } -const initialState: ItemSelectionsState = Object.create(null); +const initialState: ObjectSelectionsState = Object.create(null); /** * Performs a search filter action on the current state @@ -23,11 +23,11 @@ const initialState: ItemSelectionsState = Object.create(null); * @param {SearchFilterAction} action The action that should be performed * @returns {SearchFiltersState} The state after the action is performed */ -export function itemSelectionReducer(state = initialState, action: ItemSelectionAction): ItemSelectionsState { +export function objectSelectionReducer(state = initialState, action: ObjectSelectionAction): ObjectSelectionsState { switch (action.type) { - case ItemSelectionActionTypes.INITIAL_SELECT: { + case ObjectSelectionActionTypes.INITIAL_SELECT: { if (isEmpty(state) || isEmpty(state[action.id])) { return Object.assign({}, state, { [action.id]: { @@ -38,7 +38,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection return state; } - case ItemSelectionActionTypes.INITIAL_DESELECT: { + case ObjectSelectionActionTypes.INITIAL_DESELECT: { if (isEmpty(state) || isEmpty(state[action.id])) { return Object.assign({}, state, { [action.id]: { @@ -49,7 +49,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection return state; } - case ItemSelectionActionTypes.SELECT: { + case ObjectSelectionActionTypes.SELECT: { return Object.assign({}, state, { [action.id]: { checked: true @@ -57,7 +57,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection }); } - case ItemSelectionActionTypes.DESELECT: { + case ObjectSelectionActionTypes.DESELECT: { return Object.assign({}, state, { [action.id]: { checked: false @@ -65,7 +65,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection }); } - case ItemSelectionActionTypes.SWITCH: { + case ObjectSelectionActionTypes.SWITCH: { return Object.assign({}, state, { [action.id]: { checked: (isEmpty(state) || isEmpty(state[action.id])) ? true : !state[action.id].checked @@ -73,7 +73,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection }); } - case ItemSelectionActionTypes.RESET: { + case ObjectSelectionActionTypes.RESET: { return {}; } diff --git a/src/app/shared/object-select/object-select.service.spec.ts b/src/app/shared/object-select/object-select.service.spec.ts new file mode 100644 index 0000000000..3b5bcec06f --- /dev/null +++ b/src/app/shared/object-select/object-select.service.spec.ts @@ -0,0 +1,96 @@ +import { ObjectSelectService } from './object-select.service'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { ObjectSelectionsState } from './object-select.reducer'; +import { AppState } from '../../app.reducer'; +import { + ObjectSelectionDeselectAction, + ObjectSelectionInitialDeselectAction, + ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, + ObjectSelectionSelectAction, ObjectSelectionSwitchAction +} from './object-select.actions'; + +describe('ObjectSelectService', () => { + let service: ObjectSelectService; + + const mockObjectId = 'id1'; + + const store: Store = jasmine.createSpyObj('store', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + + const appStore: Store = jasmine.createSpyObj('appStore', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + + beforeEach(() => { + service = new ObjectSelectService(store, appStore); + }); + + describe('when the initialSelect method is triggered', () => { + beforeEach(() => { + service.initialSelect(mockObjectId); + }); + + it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialSelectAction(mockObjectId)); + }); + }); + + describe('when the initialDeselect method is triggered', () => { + beforeEach(() => { + service.initialDeselect(mockObjectId); + }); + + it('ObjectSelectionInitialDeselectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialDeselectAction(mockObjectId)); + }); + }); + + describe('when the select method is triggered', () => { + beforeEach(() => { + service.select(mockObjectId); + }); + + it('ObjectSelectionSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionSelectAction(mockObjectId)); + }); + }); + + describe('when the deselect method is triggered', () => { + beforeEach(() => { + service.deselect(mockObjectId); + }); + + it('ObjectSelectionDeselectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionDeselectAction(mockObjectId)); + }); + }); + + describe('when the switch method is triggered', () => { + beforeEach(() => { + service.switch(mockObjectId); + }); + + it('ObjectSelectionSwitchAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionSwitchAction(mockObjectId)); + }); + }); + + describe('when the reset method is triggered', () => { + beforeEach(() => { + service.reset(); + }); + + it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionResetAction(null)); + }); + }); + +}); diff --git a/src/app/shared/object-select/object-select.service.ts b/src/app/shared/object-select/object-select.service.ts new file mode 100644 index 0000000000..adc394d4e1 --- /dev/null +++ b/src/app/shared/object-select/object-select.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@angular/core'; +import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { ObjectSelectionsState, ObjectSelectionState } from './object-select.reducer'; +import { + ObjectSelectionDeselectAction, + ObjectSelectionInitialDeselectAction, + ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, + ObjectSelectionSelectAction, ObjectSelectionSwitchAction +} from './object-select.actions'; +import { Observable } from 'rxjs/Observable'; +import { hasValue } from '../empty.util'; +import { map } from 'rxjs/operators'; +import { AppState } from '../../app.reducer'; + +const selectionStateSelector = (state: ObjectSelectionsState) => state.objectSelection; +const objectSelectionsStateSelector = (state: AppState) => state.objectSelection; + +/** + * Service that takes care of selecting and deselecting objects + */ +@Injectable() +export class ObjectSelectService { + + constructor( + private store: Store, + private appStore: Store + ) { + } + + /** + * Request the current selection of a given object + * @param {string} id The UUID of the object + * @returns {Observable} Emits the current selection state of the given object, if it's unavailable, return false + */ + getSelected(id: string): Observable { + return this.store.select(selectionByIdSelector(id)).pipe( + map((object: ObjectSelectionState) => { + if (object) { + return object.checked; + } else { + return false; + } + }) + ); + } + + /** + * Request the current selection of a given object + * @param {string} id The UUID of the object + * @returns {Observable} Emits the current selection state of the given object, if it's unavailable, return false + */ + getAllSelected(): Observable { + return this.appStore.select(objectSelectionsStateSelector).pipe( + map((state: ObjectSelectionsState) => Object.keys(state).filter((key) => state[key].checked)) + ); + } + + /** + * Dispatches an initial select action to the store for a given object + * @param {string} id The UUID of the object to select + */ + public initialSelect(id: string): void { + this.store.dispatch(new ObjectSelectionInitialSelectAction(id)); + } + + /** + * Dispatches an initial deselect action to the store for a given object + * @param {string} id The UUID of the object to deselect + */ + public initialDeselect(id: string): void { + this.store.dispatch(new ObjectSelectionInitialDeselectAction(id)); + } + + /** + * Dispatches a select action to the store for a given object + * @param {string} id The UUID of the object to select + */ + public select(id: string): void { + this.store.dispatch(new ObjectSelectionSelectAction(id)); + } + + /** + * Dispatches a deselect action to the store for a given object + * @param {string} id The UUID of the object to deselect + */ + public deselect(id: string): void { + this.store.dispatch(new ObjectSelectionDeselectAction(id)); + } + + /** + * Dispatches a switch action to the store for a given object + * @param {string} id The UUID of the object to select + */ + public switch(id: string): void { + this.store.dispatch(new ObjectSelectionSwitchAction(id)); + } + + /** + * Dispatches a reset action to the store for all objects + */ + public reset(): void { + this.store.dispatch(new ObjectSelectionResetAction(null)); + } + +} + +function selectionByIdSelector(id: string): MemoizedSelector { + return keySelector(id); +} + +export function keySelector(key: string): MemoizedSelector { + return createSelector(selectionStateSelector, (state: ObjectSelectionState) => { + if (hasValue(state)) { + return state[key]; + } else { + return undefined; + } + }); +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c5c6cad09b..a10855bf5e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -83,7 +83,7 @@ import { InputSuggestionsComponent } from './input-suggestions/input-suggestions import { CapitalizePipe } from './utils/capitalize.pipe'; import { MomentModule } from 'angular2-moment'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; -import { ItemSelectComponent } from './item-select/item-select.component'; +import { ItemSelectComponent } from './object-select/item-select/item-select.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here diff --git a/src/app/shared/testing/item-select-service-stub.ts b/src/app/shared/testing/object-select-service-stub.ts similarity index 94% rename from src/app/shared/testing/item-select-service-stub.ts rename to src/app/shared/testing/object-select-service-stub.ts index 690d1e1435..f4bcccae77 100644 --- a/src/app/shared/testing/item-select-service-stub.ts +++ b/src/app/shared/testing/object-select-service-stub.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs/Observable'; -export class ItemSelectServiceStub { +export class ObjectSelectServiceStub { ids: string[] = [];