diff --git a/platform/core/src/redux/reducers/viewports.js b/platform/core/src/redux/reducers/viewports.js index 2a6d66ee0..e96444f09 100644 --- a/platform/core/src/redux/reducers/viewports.js +++ b/platform/core/src/redux/reducers/viewports.js @@ -11,20 +11,63 @@ import { SET_VIEWPORT_LAYOUT_AND_DATA, } from './../constants/ActionTypes.js'; -const DEFAULT_STATE = { +export const DEFAULT_STATE = { numRows: 1, numColumns: 1, activeViewportIndex: 0, layout: { - viewports: [ - { - // plugin: 'cornerstone', - }, - ], + viewports: [{}], }, viewportSpecificData: {}, }; +/** + * Take the new number of rows and columns, delete all not used viewport data and also set + * active viewport as default in case current one is not available anymore. + * + * @param {Number} numRows + * @param {Number} numColumns + * @param {Object} currentViewportSpecificData + * @returns + */ +const findActiveViewportSpecificData = ( + numRows, + numColumns, + currentViewportSpecificData = {} +) => { + const numberOfViewports = numRows * numColumns; + const viewportSpecificData = cloneDeep(currentViewportSpecificData); + + if (numberOfViewports < Object.keys(viewportSpecificData).length) { + Object.keys(viewportSpecificData).forEach(key => { + if (key > numberOfViewports - 1) { + delete viewportSpecificData[key]; + } + }); + } + + return viewportSpecificData; +}; +/** + * Take new number of rows and columns and make sure the current active viewport index is still available, if not, return the default + * + * @param {Number} numRows + * @param {Number} numColumns + * @param {Number} currentActiveViewportIndex + * @returns + */ +const getActiveViewportIndex = ( + numRows, + numColumns, + currentActiveViewportIndex +) => { + const numberOfViewports = numRows * numColumns; + + return currentActiveViewportIndex > numberOfViewports - 1 + ? DEFAULT_STATE.activeViewportIndex + : currentActiveViewportIndex; +}; + /** * The definition of a viewport action. * @@ -50,7 +93,12 @@ const viewports = (state = DEFAULT_STATE, action) => { * @return {Object} New state. */ case SET_VIEWPORT_ACTIVE: { - return { ...state, activeViewportIndex: action.viewportIndex }; + const activeViewportIndex = getActiveViewportIndex( + state.numRows, + state.numColumns, + action.viewportIndex + ); + return { ...state, activeViewportIndex }; } /** @@ -59,11 +107,25 @@ const viewports = (state = DEFAULT_STATE, action) => { * @return {Object} New state. */ case SET_VIEWPORT_LAYOUT: { + const { numRows, numColumns } = action; + const viewportSpecificData = findActiveViewportSpecificData( + numRows, + numColumns, + state.viewportSpecificData + ); + const activeViewportIndex = getActiveViewportIndex( + numRows, + numColumns, + state.activeViewportIndex + ); + return { ...state, numRows: action.numRows, numColumns: action.numColumns, layout: { viewports: [...action.viewports] }, + viewportSpecificData, + activeViewportIndex, }; } @@ -73,12 +135,25 @@ const viewports = (state = DEFAULT_STATE, action) => { * @return {Object} New state. */ case SET_VIEWPORT_LAYOUT_AND_DATA: { + const { numRows, numColumns } = action; + const viewportSpecificData = findActiveViewportSpecificData( + numRows, + numColumns, + action.viewportSpecificData + ); + const activeViewportIndex = getActiveViewportIndex( + numRows, + numColumns, + state.activeViewportIndex + ); + return { ...state, numRows: action.numRows, numColumns: action.numColumns, layout: { viewports: [...action.viewports] }, - viewportSpecificData: cloneDeep(action.viewportSpecificData), + viewportSpecificData, + activeViewportIndex, }; } diff --git a/platform/core/src/redux/reducers/viewports.test.js b/platform/core/src/redux/reducers/viewports.test.js index 879fabc23..c7d3fe5a2 100644 --- a/platform/core/src/redux/reducers/viewports.test.js +++ b/platform/core/src/redux/reducers/viewports.test.js @@ -1,35 +1,61 @@ -// https://medium.com/@netxm/testing-redux-reducers-with-jest-6653abbfe3e1 -import reducer from './viewports.js'; +import { Reducer } from 'redux-testkit'; + +import reducer, { DEFAULT_STATE } from './viewports.js'; import * as types from './../constants/ActionTypes.js'; describe('viewports reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual({ - activeViewportIndex: 0, - numRows: 1, - numColumns: 1, - layout: { - viewports: [{}], - }, - viewportSpecificData: {}, - }); + expect(reducer(undefined, {})).toEqual(DEFAULT_STATE); }); - it('should handle SET_VIEWPORT_ACTIVE', () => { - const setViewportActiveAction = { + it('should handle SET_VIEWPORT_ACTIVE with inexistent viewport index', () => { + const initialState = { + numRows: 4, + numColumns: 4, + activeViewportIndex: 0, + }; + + const action = { type: types.SET_VIEWPORT_ACTIVE, viewportIndex: 100, }; - const updatedState = reducer({}, setViewportActiveAction); + const expectedToChangeInState = { + activeViewportIndex: 0, + }; - expect(updatedState.activeViewportIndex).toEqual( - setViewportActiveAction.viewportIndex - ); + Reducer(reducer) + .withState(initialState) + .expect(action) + .toChangeInState(expectedToChangeInState); + }); + + it('should handle SET_VIEWPORT_ACTIVE with existent viewport index', () => { + const initialState = { + numRows: 4, + numColumns: 4, + activeViewportIndex: 0, + }; + + const action = { + type: types.SET_VIEWPORT_ACTIVE, + viewportIndex: 5, + }; + + const expectedToChangeInState = { + activeViewportIndex: 5, + }; + + Reducer(reducer) + .withState(initialState) + .expect(action) + .toChangeInState(expectedToChangeInState); }); it('should handle SET_VIEWPORT_LAYOUT', () => { - const setViewportLayoutAction = { + const initialState = DEFAULT_STATE; + + const action = { type: types.SET_VIEWPORT_LAYOUT, numRows: 1, numColumns: 2, @@ -43,18 +69,193 @@ describe('viewports reducer', () => { ], }; - const updatedState = reducer({}, setViewportLayoutAction); + const expectedToChangeInState = { + numRows: 1, + numColumns: 2, + layout: { + viewports: [ + { + plugin: 'cornerstone', + }, + { + plugin: 'vtk', + }, + ], + }, + }; - expect(updatedState.numRows).toEqual(setViewportLayoutAction.numRows); - expect(updatedState.numColumns).toEqual(setViewportLayoutAction.numColumns); - expect(updatedState.layout.viewports).toEqual( - setViewportLayoutAction.viewports - ); + Reducer(reducer) + .withState(initialState) + .expect(action) + .toChangeInState(expectedToChangeInState); + }); + + it('should handle SET_VIEWPORT_LAYOUT when we reduce the number of viewports', () => { + const initialState = { + numRows: 1, + numColumns: 2, + viewportSpecificData: { + 0: { viewportData0: 'viewportData0' }, + 1: { viewportData1: 'viewportData1' }, + }, + layout: { + viewports: [], + }, + activeViewportIndex: 0, + }; + + const action = { + type: types.SET_VIEWPORT_LAYOUT, + numRows: 1, + numColumns: 1, + viewports: [], + }; + + const expectedState = { + numRows: 1, + numColumns: 1, + viewportSpecificData: { + 0: { viewportData0: 'viewportData0' }, + }, + layout: { + viewports: [], + }, + activeViewportIndex: 0, + }; + + Reducer(reducer) + .withState(initialState) + .expect(action) + .toReturnState(expectedState); + }); + + it('should handle SET_VIEWPORT_LAYOUT_AND_DATA', () => { + const initialState = { + numRows: 1, + numColumns: 1, + viewportSpecificData: { + 0: { viewportData0: 'data0' }, + }, + layout: { + viewports: [{ plugin: 'cornerstone' }], + }, + activeViewportIndex: 0, + }; + + const action = { + type: types.SET_VIEWPORT_LAYOUT_AND_DATA, + numRows: 1, + numColumns: 2, + viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], + viewportSpecificData: { + 0: { viewportData0: 'NEWdata0' }, + 1: { viewportData1: 'NEWdata1' }, + }, + }; + + const expectedState = { + numRows: 1, + numColumns: 2, + viewportSpecificData: { + 0: { viewportData0: 'NEWdata0' }, + 1: { viewportData1: 'NEWdata1' }, + }, + layout: { + viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], + }, + activeViewportIndex: 0, + }; + + Reducer(reducer) + .withState(initialState) + .expect(action) + .toReturnState(expectedState); + }); + + it('should handle SET_VIEWPORT_LAYOUT_AND_DATA when we reduce the number of viewports', () => { + const initialState = { + numRows: 1, + numColumns: 3, + viewportSpecificData: { + 0: { viewportData0: 'vtkData0' }, + 1: { viewportData1: 'vtkData1' }, + 2: { viewportData2: 'vtkData2' }, + }, + layout: { + viewports: [{ plugin: 'vtk' }, { plugin: 'vtk' }, { plugin: 'vtk' }], + }, + activeViewportIndex: 0, + }; + + const action = { + type: types.SET_VIEWPORT_LAYOUT_AND_DATA, + numRows: 1, + numColumns: 1, + viewports: [{ plugin: 'cornerstone' }], + viewportSpecificData: { + 0: { viewportData0: 'cornerstoneData0' }, + }, + }; + + const expectedState = { + numRows: 1, + numColumns: 1, + viewportSpecificData: { + 0: { viewportData0: 'cornerstoneData0' }, + }, + layout: { + viewports: [{ plugin: 'cornerstone' }], + }, + activeViewportIndex: 0, + }; + + Reducer(reducer) + .withState(initialState) + .expect(action) + .toReturnState(expectedState); + }); + + it('should handle SET_VIEWPORT when we only set one viewport specific data', () => { + const initialState = { + numRows: 1, + numColumns: 2, + viewportSpecificData: { + 0: { viewportData0: 'data0' }, + 1: { viewportData1: 'data1' }, + }, + layout: { + viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], + }, + activeViewportIndex: 0, + }; + + const action = { + type: types.SET_VIEWPORT, + viewportIndex: 1, + viewportSpecificData: { + viewportData1: 'NEWdata1', + }, + }; + + const expectedState = { + numRows: 1, + numColumns: 2, + viewportSpecificData: { + 0: { viewportData0: 'data0' }, + 1: { viewportData1: 'NEWdata1' }, + }, + layout: { + viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }], + }, + activeViewportIndex: 0, + }; + + Reducer(reducer) + .withState(initialState) + .expect(action) + .toReturnState(expectedState); }); - // If there were previous keys, this would have - // "merge" behavior, not a clear & set - // May be worth another test? it('should handle SET_VIEWPORT', () => { const viewportToSet = 0; const setViewportAction = {