55946: Refactored item-select to object-select to allow for easier implementation of collection-select

This commit is contained in:
Kristof De Langhe
2018-10-09 17:16:43 +02:00
parent 7021527f5c
commit c4203f25d5
17 changed files with 434 additions and 440 deletions

View File

@@ -14,7 +14,7 @@ import {
} from './+search-page/search-filters/search-filter/search-filter.reducer'; } from './+search-page/search-filters/search-filter/search-filter.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; 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 { export interface AppState {
router: fromRouter.RouterReducerState; router: fromRouter.RouterReducerState;
@@ -25,7 +25,7 @@ export interface AppState {
searchSidebar: SearchSidebarState; searchSidebar: SearchSidebarState;
searchFilter: SearchFiltersState; searchFilter: SearchFiltersState;
truncatable: TruncatablesState, truncatable: TruncatablesState,
itemSelection: ItemSelectionsState objectSelection: ObjectSelectionsState
} }
export const appReducers: ActionReducerMap<AppState> = { export const appReducers: ActionReducerMap<AppState> = {
@@ -37,7 +37,7 @@ export const appReducers: ActionReducerMap<AppState> = {
searchSidebar: sidebarReducer, searchSidebar: sidebarReducer,
searchFilter: filterReducer, searchFilter: filterReducer,
truncatable: truncatableReducer, truncatable: truncatableReducer,
itemSelection: itemSelectionReducer objectSelection: objectSelectionReducer
}; };
export const routerStateSelector = (state: AppState) => state.router; export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -64,8 +64,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
import { UploaderService } from '../shared/uploader/uploader.service'; import { UploaderService } from '../shared/uploader/uploader.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
import { DSpaceObjectDataService } from './data/dspace-object-data.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 { MappingCollectionsReponseParsingService } from './data/mapping-collections-reponse-parsing.service';
import { ObjectSelectService } from '../shared/object-select/object-select.service';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -131,7 +131,7 @@ const PROVIDERS = [
UploaderService, UploaderService,
UUIDService, UUIDService,
DSpaceObjectDataService, DSpaceObjectDataService,
ItemSelectService, ObjectSelectService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,

View File

@@ -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 */

View File

@@ -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({});
});
});

View File

@@ -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<ItemSelectionsState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */
dispatch: {},
/* tslint:enable:no-empty */
select: Observable.of(true)
});
const appStore: Store<AppState> = 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));
});
});
});

View File

@@ -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<ItemSelectionsState>,
private appStore: Store<AppState>
) {
}
/**
* Request the current selection of a given item
* @param {string} id The UUID of the item
* @returns {Observable<boolean>} Emits the current selection state of the given item, if it's unavailable, return false
*/
getSelected(id: string): Observable<boolean> {
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<boolean>} Emits the current selection state of the given item, if it's unavailable, return false
*/
getAllSelected(): Observable<string[]> {
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<ItemSelectionsState, ItemSelectionState> {
return keySelector<ItemSelectionState>(id);
}
export function keySelector<T>(key: string): MemoizedSelector<ItemSelectionsState, T> {
return createSelector(selectionStateSelector, (state: ItemSelectionState) => {
if (hasValue(state)) {
return state[key];
} else {
return undefined;
}
});
}

View File

@@ -1,30 +1,25 @@
import { ItemSelectComponent } from './item-select.component'; import { ItemSelectComponent } from './item-select.component';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; 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 { 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', () => { describe('ItemSelectComponent', () => {
let comp: ItemSelectComponent; let comp: ItemSelectComponent;
let fixture: ComponentFixture<ItemSelectComponent>; let fixture: ComponentFixture<ItemSelectComponent>;
let itemSelectService: ItemSelectService; let itemSelectService: ObjectSelectService;
const mockItemList = [ const mockItemList = [
Object.assign(new Item(), { Object.assign(new Item(), {
@@ -70,7 +65,7 @@ describe('ItemSelectComponent', () => {
imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])],
declarations: [], declarations: [],
providers: [ providers: [
{ provide: ItemSelectService, useValue: new ItemSelectServiceStub() }, { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) } { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,12 +1,11 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; 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 { 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({ @Component({
selector: 'ds-item-select', selector: 'ds-item-select',
@@ -50,11 +49,11 @@ export class ItemSelectComponent implements OnInit {
*/ */
selectedIds$: Observable<string[]>; selectedIds$: Observable<string[]>;
constructor(private itemSelectService: ItemSelectService) { constructor(private objectelectService: ObjectSelectService) {
} }
ngOnInit(): void { ngOnInit(): void {
this.selectedIds$ = this.itemSelectService.getAllSelected(); this.selectedIds$ = this.objectelectService.getAllSelected();
} }
/** /**
@@ -62,7 +61,7 @@ export class ItemSelectComponent implements OnInit {
* @param {string} id * @param {string} id
*/ */
switch(id: string) { switch(id: string) {
this.itemSelectService.switch(id); this.objectelectService.switch(id);
} }
/** /**
@@ -71,7 +70,7 @@ export class ItemSelectComponent implements OnInit {
* @returns {Observable<boolean>} * @returns {Observable<boolean>}
*/ */
getSelected(id: string): Observable<boolean> { getSelected(id: string): Observable<boolean> {
return this.itemSelectService.getSelected(id); return this.objectelectService.getSelected(id);
} }
/** /**
@@ -83,7 +82,7 @@ export class ItemSelectComponent implements OnInit {
take(1) take(1)
).subscribe((ids: string[]) => { ).subscribe((ids: string[]) => {
this.confirm.emit(ids); this.confirm.emit(ids);
this.itemSelectService.reset(); this.objectelectService.reset();
}); });
} }

View File

@@ -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 */

View File

@@ -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({});
});
});

View File

@@ -1,21 +1,21 @@
import { isEmpty } from '../empty.util'; 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 * Interface that represents the state for a single filters
*/ */
export interface ItemSelectionState { export interface ObjectSelectionState {
checked: boolean; checked: boolean;
} }
/** /**
* Interface that represents the state for all available filters * Interface that represents the state for all available filters
*/ */
export interface ItemSelectionsState { export interface ObjectSelectionsState {
[id: string]: ItemSelectionState [id: string]: ObjectSelectionState
} }
const initialState: ItemSelectionsState = Object.create(null); const initialState: ObjectSelectionsState = Object.create(null);
/** /**
* Performs a search filter action on the current state * 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 * @param {SearchFilterAction} action The action that should be performed
* @returns {SearchFiltersState} The state after the action is 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) { switch (action.type) {
case ItemSelectionActionTypes.INITIAL_SELECT: { case ObjectSelectionActionTypes.INITIAL_SELECT: {
if (isEmpty(state) || isEmpty(state[action.id])) { if (isEmpty(state) || isEmpty(state[action.id])) {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.id]: { [action.id]: {
@@ -38,7 +38,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection
return state; return state;
} }
case ItemSelectionActionTypes.INITIAL_DESELECT: { case ObjectSelectionActionTypes.INITIAL_DESELECT: {
if (isEmpty(state) || isEmpty(state[action.id])) { if (isEmpty(state) || isEmpty(state[action.id])) {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.id]: { [action.id]: {
@@ -49,7 +49,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection
return state; return state;
} }
case ItemSelectionActionTypes.SELECT: { case ObjectSelectionActionTypes.SELECT: {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.id]: { [action.id]: {
checked: true checked: true
@@ -57,7 +57,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection
}); });
} }
case ItemSelectionActionTypes.DESELECT: { case ObjectSelectionActionTypes.DESELECT: {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.id]: { [action.id]: {
checked: false checked: false
@@ -65,7 +65,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection
}); });
} }
case ItemSelectionActionTypes.SWITCH: { case ObjectSelectionActionTypes.SWITCH: {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.id]: { [action.id]: {
checked: (isEmpty(state) || isEmpty(state[action.id])) ? true : !state[action.id].checked 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 {}; return {};
} }

View File

@@ -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<ObjectSelectionsState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */
dispatch: {},
/* tslint:enable:no-empty */
select: Observable.of(true)
});
const appStore: Store<AppState> = 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));
});
});
});

View File

@@ -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<ObjectSelectionsState>,
private appStore: Store<AppState>
) {
}
/**
* Request the current selection of a given object
* @param {string} id The UUID of the object
* @returns {Observable<boolean>} Emits the current selection state of the given object, if it's unavailable, return false
*/
getSelected(id: string): Observable<boolean> {
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<boolean>} Emits the current selection state of the given object, if it's unavailable, return false
*/
getAllSelected(): Observable<string[]> {
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<ObjectSelectionsState, ObjectSelectionState> {
return keySelector<ObjectSelectionState>(id);
}
export function keySelector<T>(key: string): MemoizedSelector<ObjectSelectionsState, T> {
return createSelector(selectionStateSelector, (state: ObjectSelectionState) => {
if (hasValue(state)) {
return state[key];
} else {
return undefined;
}
});
}

View File

@@ -83,7 +83,7 @@ import { InputSuggestionsComponent } from './input-suggestions/input-suggestions
import { CapitalizePipe } from './utils/capitalize.pipe'; import { CapitalizePipe } from './utils/capitalize.pipe';
import { MomentModule } from 'angular2-moment'; import { MomentModule } from 'angular2-moment';
import { ObjectKeysPipe } from './utils/object-keys-pipe'; 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 = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here

View File

@@ -1,6 +1,6 @@
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
export class ItemSelectServiceStub { export class ObjectSelectServiceStub {
ids: string[] = []; ids: string[] = [];