diff --git a/__tests__/src/actions/companionWindow.test.js b/__tests__/src/actions/companionWindow.test.js new file mode 100644 index 0000000000000000000000000000000000000000..fd28ad7cd4482b1d8cb511f80e699de1fd1f18f7 --- /dev/null +++ b/__tests__/src/actions/companionWindow.test.js @@ -0,0 +1,53 @@ +import * as actions from '../../../src/state/actions'; +import ActionTypes from '../../../src/state/actions/action-types'; + +describe('companionWindow actions', () => { + describe('addCompanionWindow', () => { + it('should return correct action object', () => { + const payload = { + content: 'info', + position: 'right', + foo: 'bar', + }; + const action = actions.addCompanionWindow(payload); + expect(action.type).toBe(ActionTypes.ADD_COMPANION_WINDOW); + expect(action.payload).toEqual(payload); + }); + + it('should set the correct default values', () => { + const payload = {}; + const defaults = { foo: 'bar' }; + const action = actions.addCompanionWindow(payload, defaults); + expect(action.payload.foo).toBe('bar'); + }); + + it('should generate a new companionWindow ID', () => { + const payload = {}; + + expect(actions.addCompanionWindow(payload).id).toEqual( + expect.stringMatching(/^cw-\w+-\w+/), + ); + }); + }); + + describe('updateCompanionWindow', () => { + it('should return correct action object', () => { + const payload = { + content: 'info', + position: 'right', + }; + const action = actions.updateCompanionWindow('cw-123', payload); + expect(action.type).toBe(ActionTypes.UPDATE_COMPANION_WINDOW); + expect(action.id).toBe('cw-123'); + expect(action.payload).toEqual(payload); + }); + }); + + describe('removeCompanionWindow', () => { + it('should return correct action object', () => { + const action = actions.removeCompanionWindow('cw-123'); + expect(action.type).toBe(ActionTypes.REMOVE_COMPANION_WINDOW); + expect(action.id).toBe('cw-123'); + }); + }); +}); diff --git a/__tests__/src/actions/window.test.js b/__tests__/src/actions/window.test.js index 868fbf52c06131dac741954f7bbd50e610adf4f8..2dde5c7c11ba78e4f83b9e9cc8e93b369f3dbf30 100644 --- a/__tests__/src/actions/window.test.js +++ b/__tests__/src/actions/window.test.js @@ -15,6 +15,7 @@ describe('window actions', () => { id: 'helloworld', canvasIndex: 1, collectionIndex: 0, + companionWindowIds: [], manifestId: null, rangeId: null, thumbnailNavigationPosition: 'bottom', @@ -26,6 +27,20 @@ describe('window actions', () => { expect(actions.addWindow(options)).toEqual(expectedAction); }); }); + + describe('updateWindow', () => { + it('should return correct action object', () => { + const payload = { + foo: 1, + bar: 2, + }; + const action = actions.updateWindow('window-123', payload); + expect(action.type).toBe(ActionTypes.UPDATE_WINDOW); + expect(action.id).toBe('window-123'); + expect(action.payload).toEqual(payload); + }); + }); + describe('removeWindow', () => { it('removes the window and returns windowId', () => { const id = 'abc123'; @@ -85,36 +100,43 @@ describe('window actions', () => { }); }); - describe('setWindowCompanionWindow', () => { - it('returns the appropriate action type', () => { - const windowId = 'abc123'; - const panelType = 'info'; - const position = 'right'; - const expectedAction = { - type: ActionTypes.SET_WINDOW_COMPANION_WINDOW, - windowId, - panelType, - position, - }; - expect(actions.setWindowCompanionWindow(windowId, 'info', 'right')).toEqual(expectedAction); - }); - }); - describe('popOutCompanionWindow', () => { it('returns a thunk which dispatches the appropriate actions', () => { - const mockDispatch = jest.fn(); + const mockState = { + windows: { + abc123: { + companionWindowIds: ['cw-1'], + }, + }, + }; + const mockDispatch = jest.fn(() => ({ id: 'cw-1' })); + const mockGetState = jest.fn(() => mockState); const windowId = 'abc123'; const panelType = 'info'; const position = 'right'; const thunk = actions.popOutCompanionWindow(windowId, panelType, position); expect(typeof thunk).toEqual('function'); - thunk(mockDispatch); - expect(mockDispatch).toHaveBeenCalledTimes(2); - expect(mockDispatch).toHaveBeenCalledWith({ - type: ActionTypes.SET_WINDOW_COMPANION_WINDOW, windowId, panelType, position, + thunk(mockDispatch, mockGetState); + expect(mockDispatch).toHaveBeenCalledTimes(4); + + expect(mockDispatch).toHaveBeenNthCalledWith(1, { + type: ActionTypes.REMOVE_COMPANION_WINDOW, + id: 'cw-1', }); - expect(mockDispatch).toHaveBeenCalledWith({ + + const addCompanionWindowAction = mockDispatch.mock.calls[1][0]; + expect(addCompanionWindowAction.type).toBe(ActionTypes.ADD_COMPANION_WINDOW); + expect(addCompanionWindowAction.payload).toEqual({ content: 'info', position: 'right' }); + expect(addCompanionWindowAction.id.startsWith('cw-')).toBe(true); + + expect(mockDispatch).toHaveBeenNthCalledWith(3, { + type: ActionTypes.UPDATE_WINDOW, + id: 'abc123', + payload: { companionWindowIds: ['cw-1'] }, + }); + + expect(mockDispatch).toHaveBeenNthCalledWith(4, { type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR_PANEL, windowId, panelType: 'closed', }); }); diff --git a/__tests__/src/components/CompanionWindow.test.js b/__tests__/src/components/CompanionWindow.test.js index 02dd89f090918704881adafc1e07f7626abec71f..f155abd8ef11cdfcad8ea38296e1f6fee67137bb 100644 --- a/__tests__/src/components/CompanionWindow.test.js +++ b/__tests__/src/components/CompanionWindow.test.js @@ -7,8 +7,10 @@ import WindowSideBarInfoPanel from '../../../src/containers/WindowSideBarInfoPan function createWrapper(props) { return shallow( <CompanionWindow - windowId="abc123" + id="abc123" + windowId="x" classes={{}} + companionWindow={{}} position="right" {...props} />, @@ -20,7 +22,7 @@ describe('CompanionWindow', () => { describe('when the panelContent is set to "info"', () => { it('renders the WindowSideBarInfoPanel', () => { - companionWindow = createWrapper({ panelContent: 'info' }); + companionWindow = createWrapper({ content: 'info' }); expect(companionWindow.find(WindowSideBarInfoPanel).length).toBe(1); }); }); @@ -33,16 +35,16 @@ describe('CompanionWindow', () => { }); describe('when the close companion window button is clicked', () => { - it('triggers the closeCompanionWindow prop with the appropriate args', () => { - const closeCompanionWindowEvent = jest.fn(); + it('triggers the onCloseClick prop with the appropriate args', () => { + const removeCompanionWindowEvent = jest.fn(); companionWindow = createWrapper({ - closeCompanionWindow: closeCompanionWindowEvent, + onCloseClick: removeCompanionWindowEvent, }); const closeButton = companionWindow.find('WithStyles(IconButton)[aria-label="closeCompanionWindow"]'); closeButton.simulate('click'); - expect(closeCompanionWindowEvent).toHaveBeenCalledTimes(1); - expect(closeCompanionWindowEvent).toHaveBeenCalledWith('abc123', null, 'right'); + expect(removeCompanionWindowEvent).toHaveBeenCalledTimes(1); + expect(removeCompanionWindowEvent).toHaveBeenCalledWith('x', 'abc123'); }); }); }); diff --git a/__tests__/src/components/WindowMiddleContent.test.js b/__tests__/src/components/WindowMiddleContent.test.js index 34e4bdbf9e1097bae55c96e9e522bc6a4dc713b8..64da2c84b34af25fa14cb51b23809348ef418e74 100644 --- a/__tests__/src/components/WindowMiddleContent.test.js +++ b/__tests__/src/components/WindowMiddleContent.test.js @@ -5,24 +5,34 @@ import CompanionWindow from '../../../src/containers/CompanionWindow'; import WindowSideBar from '../../../src/containers/WindowSideBar'; import WindowViewer from '../../../src/containers/WindowViewer'; +/** create wrapper */ +function createWrapper(props) { + return shallow( + <WindowMiddleContent + companionWindowIds={['cw1', 'cw-2']} + window={{ id: 'window-1' }} + manifest={{}} + {...props} + />, + ); +} + describe('WindowMiddleContent', () => { - let wrapper; - let manifest; it('should render outer element', () => { - wrapper = shallow(<WindowMiddleContent window={window} />); + const wrapper = createWrapper(); expect(wrapper.find('.mirador-window-middle-content')).toHaveLength(1); }); - it('should render <CompanionWindow>', () => { - wrapper = shallow(<WindowMiddleContent window={window} />); - expect(wrapper.find(CompanionWindow)).toHaveLength(1); + it('should render all <CompanionWindow> components', () => { + const wrapper = createWrapper(); + expect(wrapper.find(CompanionWindow)).toHaveLength(2); }); it('should render <WindowSideBar>', () => { - wrapper = shallow(<WindowMiddleContent window={window} />); + const wrapper = createWrapper(); expect(wrapper.find(WindowSideBar)).toHaveLength(1); }); it('should render <WindowViewer> if manifest is present', () => { - manifest = { id: 456, isFetching: false }; - wrapper = shallow(<WindowMiddleContent window={window} manifest={manifest} />); + const manifest = { id: 456, isFetching: false }; + const wrapper = createWrapper({ manifest }); expect(wrapper.find(WindowViewer)).toHaveLength(1); }); }); diff --git a/__tests__/src/components/WindowTopBar.test.js b/__tests__/src/components/WindowTopBar.test.js index e482f635c53eeb19ff208026f008458e76c9ef3d..4fe237023b1c07600e28eb782d73d9eea2941d25 100644 --- a/__tests__/src/components/WindowTopBar.test.js +++ b/__tests__/src/components/WindowTopBar.test.js @@ -20,7 +20,7 @@ function createWrapper(props) { windowId="xyz" classes={{}} t={str => str} - removeWindow={() => {}} + closeWindow={() => {}} toggleWindowSideBar={() => {}} {...props} />, @@ -67,8 +67,8 @@ describe('WindowTopBar', () => { }); it('passes correct props to <Button/>', () => { - const removeWindow = jest.fn(); - const wrapper = createWrapper({ removeWindow }); - expect(wrapper.find(IconButton).last().props().onClick).toBe(removeWindow); + const closeWindow = jest.fn(); + const wrapper = createWrapper({ closeWindow }); + expect(wrapper.find(IconButton).last().props().onClick).toBe(closeWindow); }); }); diff --git a/__tests__/src/reducers/companionWindows.test.js b/__tests__/src/reducers/companionWindows.test.js new file mode 100644 index 0000000000000000000000000000000000000000..68775833910068ef0252fdf95084980790c37c1a --- /dev/null +++ b/__tests__/src/reducers/companionWindows.test.js @@ -0,0 +1,63 @@ +import { companionWindowsReducer } from '../../../src/state/reducers/companionWindows'; +import ActionTypes from '../../../src/state/actions/action-types'; + +describe('companionWindowsReducer', () => { + describe('ADD_COMPANION_WINDOW', () => { + it('adds a new companion window', () => { + const action = { + type: ActionTypes.ADD_COMPANION_WINDOW, + id: 'abc123', + payload: { content: 'info', position: 'right' }, + }; + const beforeState = {}; + const expectedState = { + abc123: { + position: 'right', + content: 'info', + }, + }; + expect(companionWindowsReducer(beforeState, action)).toEqual(expectedState); + }); + }); + + describe('UPDATE_COMPANION_WINDOW', () => { + it('updates an existing companion window', () => { + const action = { + type: ActionTypes.UPDATE_COMPANION_WINDOW, + id: 'abc123', + payload: { content: 'canvases', foo: 'bar' }, + }; + const beforeState = { + abc123: { + position: 'right', + content: 'info', + }, + }; + const expectedState = { + abc123: { + position: 'right', + content: 'canvases', + foo: 'bar', + }, + }; + expect(companionWindowsReducer(beforeState, action)).toEqual(expectedState); + }); + }); + + describe('REMOVE_COMPANION_WINDOW', () => { + it('should remove a companion window', () => { + const action = { + type: ActionTypes.REMOVE_COMPANION_WINDOW, + id: 'abc123', + }; + const beforeState = { + abc123: { + position: 'right', + content: 'info', + }, + }; + const expectedState = {}; + expect(companionWindowsReducer(beforeState, action)).toEqual(expectedState); + }); + }); +}); diff --git a/__tests__/src/reducers/windows.test.js b/__tests__/src/reducers/windows.test.js index 758cb5d9287014286e354b7c4b023c1925419513..e8beeabbbf756d5feda29645d26af0751986b2a8 100644 --- a/__tests__/src/reducers/windows.test.js +++ b/__tests__/src/reducers/windows.test.js @@ -121,42 +121,6 @@ describe('windows reducer', () => { }); }); - describe('SET_WINDOW_COMPANION_WINDOW', () => { - it('sets the given type under the given position when no companion window exists', () => { - const action = { - type: ActionTypes.SET_WINDOW_COMPANION_WINDOW, - windowId: 'abc123', - position: 'right', - panelType: 'info', - }; - const before = { - abc123: {}, - }; - const after = { - abc123: { companionWindows: { right: 'info' } }, - }; - - expect(windowsReducer(before, action)).toEqual(after); - }); - - it('overwrites the given position and sets the new type when a companion window in the same position exists', () => { - const action = { - type: ActionTypes.SET_WINDOW_COMPANION_WINDOW, - windowId: 'abc123', - position: 'right', - panelType: 'info', - }; - const before = { - abc123: { companionWindows: { right: 'canvas_navigation' } }, - }; - const after = { - abc123: { companionWindows: { right: 'info' } }, - }; - - expect(windowsReducer(before, action)).toEqual(after); - }); - }); - it('should handle NEXT_CANVAS', () => { expect(windowsReducer({ abc123: { @@ -230,4 +194,28 @@ describe('windows reducer', () => { }, }); }); + + describe('UPDATE_WINDOW', () => { + it('updates an existing window', () => { + const action = { + type: ActionTypes.UPDATE_WINDOW, + id: 'abc123', + payload: { foo: 11, baz: 33 }, + }; + const beforeState = { + abc123: { + foo: 1, + bar: 2, + }, + }; + const expectedState = { + abc123: { + foo: 11, + bar: 2, + baz: 33, + }, + }; + expect(windowsReducer(beforeState, action)).toEqual(expectedState); + }); + }); }); diff --git a/__tests__/src/selectors/index.test.js b/__tests__/src/selectors/index.test.js index 216d0efbd8d0a902722895314c7e082cb2ea3aba..f3f04a75fe416ca6898e2b79dcceae4866b885fd 100644 --- a/__tests__/src/selectors/index.test.js +++ b/__tests__/src/selectors/index.test.js @@ -323,17 +323,20 @@ describe('getIdAndLabelOfCanvases', () => { describe('getCompanionWindowForPosition', () => { const state = { - windows: { a: { companionWindows: { right: 'info' } } }, + companionWindows: { + abc: { id: 'abc', windowId: 'a', position: 'right' }, + xyz: { id: 'xyz', windowId: 'b', position: 'bottom' }, + }, }; it('the companion window type based on the given position', () => { const received = getCompanionWindowForPosition(state, 'a', 'right'); - expect(received).toEqual('info'); + expect(received.id).toEqual('abc'); }); it('returns undefined if the given window does not exist', () => { - const received = getCompanionWindowForPosition(state, 'b', 'right'); + const received = getCompanionWindowForPosition(state, 'c', 'right'); expect(received).toBeUndefined(); }); diff --git a/package.json b/package.json index 3378f26d956f2ebc84d388b8874de98a545c6ee0..156228a973f2ae817a969b06a9d7f2224c32a88c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "deepmerge": "^3.1.0", "dompurify": "^1.0.9", "i18next": "^14.0.1", + "immutable": "^4.0.0-rc.12", "intersection-observer": "^0.5.1", "lodash": "^4.17.11", "manifesto.js": "^3.0.9", diff --git a/src/components/CompanionWindow.js b/src/components/CompanionWindow.js index 2f73e48ef0db99c4a0d5e7ec1ecfd21b86563013..b26f2f84ce1b900d64c531b9dca218a779650b55 100644 --- a/src/components/CompanionWindow.js +++ b/src/components/CompanionWindow.js @@ -17,8 +17,9 @@ class CompanionWindow extends Component { * @return React Component */ activePanelComponent() { - const { windowId, panelContent } = this.props; - switch (panelContent) { + const { content, windowId } = this.props; + + switch (content) { case 'info': return <WindowSideBarInfoPanel windowId={windowId} />; case 'canvas_navigation': @@ -34,8 +35,9 @@ class CompanionWindow extends Component { */ render() { const { - classes, closeCompanionWindow, isDisplayed, position, t, windowId, + classes, id, onCloseClick, isDisplayed, position, t, windowId, } = this.props; + return ( <Paper className={[classes.root, ns(`companion-window-${position}`)].join(' ')} @@ -46,7 +48,7 @@ class CompanionWindow extends Component { <IconButton aria-label={t('closeCompanionWindow')} className={classes.closeButton} - onClick={() => { closeCompanionWindow(windowId, null, position); }} + onClick={() => { onCloseClick(windowId, id); }} > <CloseIcon /> </IconButton> @@ -57,18 +59,20 @@ class CompanionWindow extends Component { CompanionWindow.propTypes = { classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types, - closeCompanionWindow: PropTypes.func, + content: PropTypes.string, + id: PropTypes.string.isRequired, + onCloseClick: PropTypes.func, + position: PropTypes.string, isDisplayed: PropTypes.bool, - panelContent: PropTypes.string, - position: PropTypes.string.isRequired, t: PropTypes.func, windowId: PropTypes.string.isRequired, }; CompanionWindow.defaultProps = { - closeCompanionWindow: () => {}, - panelContent: null, + content: null, + onCloseClick: () => {}, isDisplayed: false, + position: null, t: key => key, }; diff --git a/src/components/WindowMiddleContent.js b/src/components/WindowMiddleContent.js index 8b6604f1f3344e81e7acf514ece1bfeac6e23198..b2817e6b68d508c5898c51b06e0fcde6ea68fc3d 100644 --- a/src/components/WindowMiddleContent.js +++ b/src/components/WindowMiddleContent.js @@ -32,18 +32,19 @@ class WindowMiddleContent extends Component { * Render the component */ render() { - const { window } = this.props; + const { companionWindowIds, window } = this.props; return ( <div className={ns('window-middle-content')}> <WindowSideBar windowId={window.id} /> {this.renderViewer()} - <CompanionWindow windowId={window.id} position="right" /> + { companionWindowIds.map(id => <CompanionWindow key={id} id={id} windowId={window.id} />) } </div> ); } } WindowMiddleContent.propTypes = { + companionWindowIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; diff --git a/src/components/WindowTopBar.js b/src/components/WindowTopBar.js index 7e5969cd85d2d4602eb03e57c90cba13db76d4a8..846fe95780dc0d91dd256a410a856a66bacc5f2c 100644 --- a/src/components/WindowTopBar.js +++ b/src/components/WindowTopBar.js @@ -24,7 +24,7 @@ class WindowTopBar extends Component { */ render() { const { - removeWindow, windowId, classes, toggleWindowSideBar, t, manifestTitle, + closeWindow, windowId, classes, toggleWindowSideBar, t, manifestTitle, } = this.props; return ( <AppBar position="relative"> @@ -46,7 +46,7 @@ class WindowTopBar extends Component { color="inherit" className={ns('window-close')} aria-label={t('closeWindow')} - onClick={removeWindow} + onClick={closeWindow} > <CloseIcon /> </IconButton> @@ -58,7 +58,7 @@ class WindowTopBar extends Component { WindowTopBar.propTypes = { manifestTitle: PropTypes.string, - removeWindow: PropTypes.func.isRequired, + closeWindow: PropTypes.func.isRequired, windowId: PropTypes.string.isRequired, classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types toggleWindowSideBar: PropTypes.func.isRequired, diff --git a/src/containers/CompanionWindow.js b/src/containers/CompanionWindow.js index ac5536d50546367b63818ec0eb4d5c7b341f5917..0572435bf5a9c4fd7bc012dcdf47cacbc4b96d60 100644 --- a/src/containers/CompanionWindow.js +++ b/src/containers/CompanionWindow.js @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import { withNamespaces } from 'react-i18next'; import * as actions from '../state/actions'; import miradorWithPlugins from '../lib/miradorWithPlugins'; -import { getCompanionWindowForPosition } from '../state/selectors'; import CompanionWindow from '../components/CompanionWindow'; /** @@ -11,13 +10,14 @@ import CompanionWindow from '../components/CompanionWindow'; * @memberof CompanionWindow * @private */ -const mapStateToProps = (state, { windowId, position }) => { - const companionWindowForPosition = getCompanionWindowForPosition(state, windowId, position); +const mapStateToProps = (state, { id }) => { + const companionWindow = state.companionWindows[id]; return { - isDisplayed: (companionWindowForPosition - && companionWindowForPosition.length > 0), - panelContent: companionWindowForPosition, + ...companionWindow, + isDisplayed: (companionWindow + && companionWindow.content + && companionWindow.content.length > 0), }; }; @@ -27,7 +27,7 @@ const mapStateToProps = (state, { windowId, position }) => { * @private */ const mapDispatchToProps = { - closeCompanionWindow: actions.setWindowCompanionWindow, + onCloseClick: actions.closeCompanionWindow, }; const enhance = compose( diff --git a/src/containers/WindowMiddleContent.js b/src/containers/WindowMiddleContent.js index f843d9da3b98a3ffbe5eac68a397f1b441d72ca9..9ca434593329cc514e680d4a05b0be79b0fc5860 100644 --- a/src/containers/WindowMiddleContent.js +++ b/src/containers/WindowMiddleContent.js @@ -1,10 +1,16 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; +import { getCompantionWindowIds } from '../state/selectors'; import miradorWithPlugins from '../lib/miradorWithPlugins'; import WindowMiddleContent from '../components/WindowMiddleContent'; +/** */ +const mapStateToProps = (state, { window }) => ({ + companionWindowIds: getCompantionWindowIds(state, window.id), +}); + const enhance = compose( - connect(null, null), + connect(mapStateToProps, null), miradorWithPlugins, // further HOC go here ); diff --git a/src/containers/WindowTopBar.js b/src/containers/WindowTopBar.js index 1d193b101f5f425ea21d5de7a37248d3509ab0b4..191ebb59ca3256d1f782028fc4f3583a9c30f3fb 100644 --- a/src/containers/WindowTopBar.js +++ b/src/containers/WindowTopBar.js @@ -17,7 +17,7 @@ const mapStateToProps = (state, { windowId }) => ({ * @private */ const mapDispatchToProps = (dispatch, { windowId }) => ({ - removeWindow: () => dispatch(actions.removeWindow(windowId)), + closeWindow: () => dispatch(actions.closeWindow(windowId)), toggleWindowSideBar: () => dispatch(actions.toggleWindowSideBar(windowId)), }); diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js index f68f33db70258a6b4f98f4d1ca8f1c008b55b8ed..117eda9052691186eea6ead2d20257607cc8c702 100644 --- a/src/state/actions/action-types.js +++ b/src/state/actions/action-types.js @@ -1,4 +1,9 @@ const ActionTypes = { + ADD_COMPANION_WINDOW: 'ADD_COMPANION_WINDOW', + UPDATE_COMPANION_WINDOW: 'UPDATE_COMPANION_WINDOW', + REMOVE_COMPANION_WINDOW: 'REMOVE_COMPANION_WINDOW', + UPDATE_WINDOW: 'UPDATE_WINDOW', + FOCUS_WINDOW: 'FOCUS_WINDOW', SET_WORKSPACE_FULLSCREEN: 'SET_WORKSPACE_FULLSCREEN', ADD_MANIFEST: 'ADD_MANIFEST', @@ -15,7 +20,6 @@ const ActionTypes = { SET_WINDOW_THUMBNAIL_POSITION: 'SET_WINDOW_THUMBNAIL_POSITION', SET_WINDOW_VIEW_TYPE: 'SET_WINDOW_VIEW_TYPE', SET_WORKSPACE_ADD_VISIBILITY: 'SET_WORKSPACE_ADD_VISIBILITY', - SET_WINDOW_COMPANION_WINDOW: 'SET_WINDOW_COMPANION_WINDOW', TOGGLE_WINDOW_SIDE_BAR: 'TOGGLE_WINDOW_SIDE_BAR', TOGGLE_WINDOW_SIDE_BAR_PANEL: 'TOGGLE_WINDOW_SIDE_BAR_PANEL', TOGGLE_ZOOM_CONTROLS: 'TOGGLE_ZOOM_CONTROLS', diff --git a/src/state/actions/companionWindow.js b/src/state/actions/companionWindow.js new file mode 100644 index 0000000000000000000000000000000000000000..43b27cab8db26afe77da928cf4f6b659c32cf2eb --- /dev/null +++ b/src/state/actions/companionWindow.js @@ -0,0 +1,26 @@ +import uuid from 'uuid/v4'; +import ActionTypes from './action-types'; + +const defaultProps = { + content: null, + position: null, +}; + +/** */ +export function addCompanionWindow(payload, defaults = defaultProps) { + return { + type: ActionTypes.ADD_COMPANION_WINDOW, + id: `cw-${uuid()}`, + payload: { ...defaults, ...payload }, + }; +} + +/** */ +export function updateCompanionWindow(id, payload) { + return { type: ActionTypes.UPDATE_COMPANION_WINDOW, id, payload }; +} + +/** */ +export function removeCompanionWindow(id) { + return { type: ActionTypes.REMOVE_COMPANION_WINDOW, id }; +} diff --git a/src/state/actions/index.js b/src/state/actions/index.js index 95a4dc0828066a301e77d29932c410e7c0563498..465e1fd28e821c29b4f49679fa3b1520fb3bbd02 100644 --- a/src/state/actions/index.js +++ b/src/state/actions/index.js @@ -2,6 +2,7 @@ * Action Creators for Mirador * @namespace ActionCreators */ +export * from './companionWindow'; export * from './config'; export * from './window'; export * from './manifest'; diff --git a/src/state/actions/window.js b/src/state/actions/window.js index 33b4c1b6f599adaa9e3247cc026c8611bcd427f3..afb5c766f61eaea98e0ec3b6c4edca3861826e80 100644 --- a/src/state/actions/window.js +++ b/src/state/actions/window.js @@ -1,5 +1,6 @@ import uuid from 'uuid/v4'; import ActionTypes from './action-types'; +import { addCompanionWindow, removeCompanionWindow } from './companionWindow'; /** * focusWindow - action creator @@ -26,12 +27,18 @@ export function addWindow(options) { rangeId: null, thumbnailNavigationPosition: 'bottom', // bottom by default in settings.js xywh: [0, 0, 400, 400], + companionWindowIds: [], rotation: null, view: 'single', }; return { type: ActionTypes.ADD_WINDOW, window: { ...defaultOptions, ...options } }; } +/** */ +export function updateWindow(id, payload) { + return { type: ActionTypes.UPDATE_WINDOW, id, payload }; +} + /** * removeWindow - action creator * @@ -52,25 +59,6 @@ export function toggleWindowSideBar(windowId) { return { type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR, windowId }; } -/** - * setWindowCompanionWindow - action creator - * - * @param {String} windowId - * @param {String} panelType The type of panel content to be rendered - * in the companion window (e.g. info, canvas_navigation) - * @param {String} position The position of the companion window to - * set content for (e.g. right, bottom) - * @memberof ActionCreators - */ -export function setWindowCompanionWindow(windowId, panelType, position) { - return { - type: ActionTypes.SET_WINDOW_COMPANION_WINDOW, - windowId, - panelType, - position, - }; -} - /** * toggleWindowSideBarPanel - action creator * @@ -93,10 +81,40 @@ export function toggleWindowSideBarPanel(windowId, panelType) { * @memberof ActionCreators */ export function popOutCompanionWindow(windowId, panelType, position) { - return ((dispatch) => { - dispatch(setWindowCompanionWindow(windowId, panelType, position)); + return (dispatch, getState) => { + const { companionWindowIds } = getState().windows[windowId]; + companionWindowIds.map(id => dispatch(removeCompanionWindow(id))); + + const action = dispatch(addCompanionWindow({ content: panelType, position })); + + const companionWindowId = action.id; + dispatch(updateWindow(windowId, { companionWindowIds: [companionWindowId] })); + dispatch(toggleWindowSideBarPanel(windowId, 'closed')); - }); + }; +} + +/** +* Clean up state and remove window +*/ +export function closeWindow(windowId) { + return (dispatch, getState) => { + const { companionWindowIds } = getState().windows[windowId]; + companionWindowIds.map(id => dispatch(removeCompanionWindow(id))); + dispatch(removeWindow(windowId)); + }; +} + +/** +* Close companion window and remove reference from window +*/ +export function closeCompanionWindow(windowId, companionWindowId) { + return (dispatch, getState) => { + dispatch(removeCompanionWindow(companionWindowId)); + const companionWindowIds = getState().windows[windowId].companionWindowIds + .filter(id => id !== companionWindowId); + dispatch(updateWindow(windowId, { companionWindowIds })); + }; } /** diff --git a/src/state/reducers/companionWindows.js b/src/state/reducers/companionWindows.js new file mode 100644 index 0000000000000000000000000000000000000000..c5cfc2dcc685b5570d1563974c423124591fcaf9 --- /dev/null +++ b/src/state/reducers/companionWindows.js @@ -0,0 +1,21 @@ +import { + removeIn, setIn, updateIn, merge, +} from 'immutable'; +import ActionTypes from '../actions/action-types'; + +/** */ +export function companionWindowsReducer(state = {}, action) { + switch (action.type) { + case ActionTypes.ADD_COMPANION_WINDOW: + return setIn(state, [action.id], action.payload); + + case ActionTypes.UPDATE_COMPANION_WINDOW: + return updateIn(state, [action.id], orig => merge(orig, action.payload)); + + case ActionTypes.REMOVE_COMPANION_WINDOW: + return removeIn(state, [action.id]); + + default: + return state; + } +} diff --git a/src/state/reducers/index.js b/src/state/reducers/index.js index 7a0c19af469d2af7c7fe22909ff64ecca525531a..478ac4467e511e258bce3edd51967b9d7496cc10 100644 --- a/src/state/reducers/index.js +++ b/src/state/reducers/index.js @@ -1,3 +1,4 @@ +export * from './companionWindows'; export * from './workspace'; export * from './windows'; export * from './manifests'; diff --git a/src/state/reducers/rootReducer.js b/src/state/reducers/rootReducer.js index 895fe4dbd06d9c3e98b28479eb09dfa669a585eb..927420a4fad8f8429abb6c7dbbb41da45f9e4675 100644 --- a/src/state/reducers/rootReducer.js +++ b/src/state/reducers/rootReducer.js @@ -1,5 +1,6 @@ import { combineReducers } from 'redux'; import { + companionWindowsReducer, configReducer, infoResponsesReducer, manifestsReducer, @@ -15,6 +16,7 @@ import { */ export default function createRootReducer(pluginReducers) { return combineReducers({ + companionWindows: companionWindowsReducer, workspace: workspaceReducer, windows: windowsReducer, manifests: manifestsReducer, diff --git a/src/state/reducers/windows.js b/src/state/reducers/windows.js index 2f3bb2a3e7898070a27e23dbdd5a2829c30d9ed8..7a1739f8e481d9380bd89d709ea843be247a481c 100644 --- a/src/state/reducers/windows.js +++ b/src/state/reducers/windows.js @@ -1,3 +1,4 @@ +import { updateIn, merge } from 'immutable'; import ActionTypes from '../actions/action-types'; /** @@ -7,6 +8,10 @@ export const windowsReducer = (state = {}, action) => { switch (action.type) { case ActionTypes.ADD_WINDOW: return { ...state, [action.window.id]: action.window }; + + case ActionTypes.UPDATE_WINDOW: + return updateIn(state, [action.id], orig => merge(orig, action.payload)); + case ActionTypes.REMOVE_WINDOW: return Object.keys(state).reduce((object, key) => { if (key !== action.windowId) { @@ -50,17 +55,6 @@ export const windowsReducer = (state = {}, action) => { ), }, }; - case ActionTypes.SET_WINDOW_COMPANION_WINDOW: - return { - ...state, - [action.windowId]: { - ...state[action.windowId], - companionWindows: { - ...state[action.windowId].companionWindows, - [action.position]: action.panelType, - }, - }, - }; case ActionTypes.NEXT_CANVAS: return setCanvasIndex(state, action.windowId, currentIndex => currentIndex + 1); case ActionTypes.PREVIOUS_CANVAS: diff --git a/src/state/selectors/index.js b/src/state/selectors/index.js index 557d5d3b4946455dad56fcb12a34cfd07ad6965e..e7b4565c634581f94ddc3ff0caf2e9f251c2dacd 100644 --- a/src/state/selectors/index.js +++ b/src/state/selectors/index.js @@ -188,7 +188,16 @@ export function getCanvasDescription(canvas) { * @return {String} */ export function getCompanionWindowForPosition(state, windowId, position) { - return state.windows[windowId] - && state.windows[windowId].companionWindows - && state.windows[windowId].companionWindows[position]; + return Object.values((state.companionWindows || [])).find(cw => ( + cw.windowId === windowId && cw.position === position + )); +} + +/** +* Return compantion window ids from a window +* @param {String} windowId +* @return {Array} +*/ +export function getCompantionWindowIds(state, windowId) { + return state.windows[windowId].companionWindowIds; }