diff --git a/__tests__/src/actions/window.test.js b/__tests__/src/actions/window.test.js index 11a5055027b2d9b9baf16f65d106d413bc24c132..2b8411a6c1150a8600543ee4dbbd66b248d84c59 100644 --- a/__tests__/src/actions/window.test.js +++ b/__tests__/src/actions/window.test.js @@ -20,6 +20,7 @@ describe('window actions', () => { thumbnailNavigationPosition: 'bottom', xywh: [0, 0, 400, 400], rotation: null, + view: 'single', }, }; expect(actions.addWindow(options)).toEqual(expectedAction); @@ -59,6 +60,18 @@ describe('window actions', () => { }); }); + describe('setWindowViewType', () => { + it('returns the appropriate action type', () => { + const id = 'abc123'; + const expectedAction = { + type: ActionTypes.SET_WINDOW_VIEW_TYPE, + windowId: id, + viewType: 'book', + }; + expect(actions.setWindowViewType(id, 'book')).toEqual(expectedAction); + }); + }); + describe('toggleWindowSideBarPanel', () => { it('returns the appropriate action type', () => { const windowId = 'abc123'; diff --git a/__tests__/src/components/WindowTopMenu.test.js b/__tests__/src/components/WindowTopMenu.test.js index d2b260854166e6c87d7790af4e1b1efba1dbf29f..0ad07676705bc7bb95165aa4be531e00169aa040 100644 --- a/__tests__/src/components/WindowTopMenu.test.js +++ b/__tests__/src/components/WindowTopMenu.test.js @@ -4,6 +4,7 @@ import ListItem from '@material-ui/core/ListItem'; import Menu from '@material-ui/core/Menu'; import Divider from '@material-ui/core/Divider'; import WindowThumbnailSettings from '../../../src/containers/WindowThumbnailSettings'; +import WindowViewSettings from '../../../src/containers/WindowViewSettings'; import WindowTopMenu from '../../../src/components/WindowTopMenu'; /** create wrapper */ @@ -22,9 +23,10 @@ describe('WindowTopMenu', () => { it('renders all needed elements', () => { const wrapper = createWrapper(); expect(wrapper.find(Menu).length).toBe(1); - expect(wrapper.find(ListItem).length).toBe(1); + expect(wrapper.find(ListItem).length).toBe(2); expect(wrapper.find(WindowThumbnailSettings).length).toBe(1); - expect(wrapper.find(Divider).length).toBe(1); + expect(wrapper.find(WindowViewSettings).length).toBe(1); + expect(wrapper.find(Divider).length).toBe(2); }); it('passes windowId to <WindowThumbnailSettings/>', () => { diff --git a/__tests__/src/components/WindowViewSettings.test.js b/__tests__/src/components/WindowViewSettings.test.js new file mode 100644 index 0000000000000000000000000000000000000000..cf3318c18c8c3c2be4f9a2f9333accad537382e8 --- /dev/null +++ b/__tests__/src/components/WindowViewSettings.test.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import Typography from '@material-ui/core/Typography'; +import WindowViewSettings from '../../../src/components/WindowViewSettings'; + +/** create wrapper */ +function createWrapper(props) { + return shallow( + <WindowViewSettings + windowId="xyz" + setWindowViewType={() => {}} + windowViewType="single" + {...props} + />, + ); +} + +describe('WindowViewSettings', () => { + it('renders all elements correctly', () => { + const wrapper = createWrapper(); + expect(wrapper.find(Typography).length).toBe(1); + expect(wrapper.find(RadioGroup).length).toBe(1); + const labels = wrapper.find(FormControlLabel); + expect(labels.length).toBe(2); + expect(labels.at(0).props().value).toBe('single'); + expect(labels.at(1).props().value).toBe('book'); + }); + + it('should set the correct label active', () => { + let wrapper = createWrapper({ windowViewType: 'single' }); + expect(wrapper.find(RadioGroup).props().value).toBe('single'); + wrapper = createWrapper({ windowViewType: 'book' }); + expect(wrapper.find(RadioGroup).props().value).toBe('book'); + }); + + it('updates state when the view config selection changes', () => { + const setWindowViewType = jest.fn(); + const wrapper = createWrapper({ setWindowViewType }); + wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'book' } }); + expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'book'); + wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'single' } }); + expect(setWindowViewType).toHaveBeenCalledWith('xyz', 'single'); + }); +}); diff --git a/__tests__/src/reducers/windows.test.js b/__tests__/src/reducers/windows.test.js index a8a75b6718dae4a218c7b2b06968a4e5bb541461..cddb619b09568d2a0e1dd362fbc0cfa6d2fa769e 100644 --- a/__tests__/src/reducers/windows.test.js +++ b/__tests__/src/reducers/windows.test.js @@ -65,6 +65,24 @@ describe('windows reducer', () => { expect(reducer(before, action)).toEqual(after); }); + it('should handle SET_WINDOW_VIEW_TYPE by changing the view attribute', () => { + const action = { + type: ActionTypes.SET_WINDOW_VIEW_TYPE, + windowId: 'abc123', + viewType: 'book', + }; + const before = { + abc123: { view: 'single' }, + abc321: { view: 'book' }, + }; + const after = { + abc123: { view: 'book' }, + abc321: { view: 'book' }, + }; + + expect(reducer(before, action)).toEqual(after); + }); + describe('TOGGLE_WINDOW_SIDE_BAR_PANEL', () => { it('sets the sideBarPanel value to the given value when it was changed', () => { const action = { diff --git a/__tests__/src/selectors/index.test.js b/__tests__/src/selectors/index.test.js index 4b0fe519feb18d6463ca9de74e873c60feb9188e..1e7b7a0566550ad919d686b111a01c40dfc5e21b 100644 --- a/__tests__/src/selectors/index.test.js +++ b/__tests__/src/selectors/index.test.js @@ -6,6 +6,7 @@ import { getManifestCanvases, getThumbnailNavigationPosition, getManifestTitle, + getWindowViewType, } from '../../../src/state/selectors'; @@ -114,3 +115,27 @@ describe('getManifestTitle', () => { expect(received).toBeUndefined(); }); }); + +describe('getWindowViewType', () => { + const state = { + windows: { + a: { id: 'a', view: 'single' }, + b: { id: 'b' }, + }, + }; + + it('should return view type if window exists', () => { + const received = getWindowViewType(state, 'a'); + expect(received).toBe('single'); + }); + + it('should return undefined if view type does not exist in window', () => { + const received = getWindowViewType(state, 'b'); + expect(received).toBeUndefined(); + }); + + it('should return undefined if window does not exists', () => { + const received = getWindowViewType(state, 'c'); + expect(received).toBeUndefined(); + }); +}); diff --git a/locales/en/translation.json b/locales/en/translation.json index a5e657c224f8a77e7a64b9fef968aeb3af4ba36d..da97f8eb62b348a1f82790e11f41cd4b34a91789 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -2,6 +2,7 @@ "translation": { "aboutThisItem": "About this item", "add": "Add", + "book": "Book", "bottom": "Bottom", "closeInfoCompanionWindow": "Close information companion window", "closeMenu": "Close Menu", @@ -19,11 +20,13 @@ "openWindows": "Open windows", "position": "Position", "right": "Right", + "single": "Single", "settings": "Settings", "theme": "Theme", "thumbnails": "Thumbnails", "toggleWindowSideBar": "Toggle window sidebar", "untitled": "[Untitled]", + "view": "View", "zoomIn": "Zoom in", "zoomOut": "Zoom out", "zoomReset": "Reset zoom" diff --git a/src/components/WindowTopMenu.js b/src/components/WindowTopMenu.js index b8d799abb772164ba0f2650933ee81c837c4004a..b5d5b9f7d34200118ba6c25f546b339a48055751 100644 --- a/src/components/WindowTopMenu.js +++ b/src/components/WindowTopMenu.js @@ -4,6 +4,7 @@ import Menu from '@material-ui/core/Menu'; import Divider from '@material-ui/core/Divider'; import PropTypes from 'prop-types'; import WindowThumbnailSettings from '../containers/WindowThumbnailSettings'; +import WindowViewSettings from '../containers/WindowViewSettings'; /** */ @@ -19,6 +20,10 @@ class WindowTopMenu extends Component { return ( <> <Menu id={`window-menu_${windowId}`} anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}> + <ListItem> + <WindowViewSettings windowId={windowId} /> + </ListItem> + <Divider /> <ListItem> <WindowThumbnailSettings windowId={windowId} /> </ListItem> diff --git a/src/components/WindowViewSettings.js b/src/components/WindowViewSettings.js new file mode 100644 index 0000000000000000000000000000000000000000..c3f5faa0ea8ff5ab38c71e2778a65b0f20e307cf --- /dev/null +++ b/src/components/WindowViewSettings.js @@ -0,0 +1,78 @@ +import React, { Component } from 'react'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Radio from '@material-ui/core/Radio'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import Typography from '@material-ui/core/Typography'; +import PhotoIcon from '@material-ui/icons/Photo'; +import ViewAgendaIcon from '@material-ui/icons/ViewAgenda'; +import PropTypes from 'prop-types'; + +/** + * + */ +export default class WindowViewSettings extends Component { + /** + * constructor - + */ + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + /** + * @private + */ + handleChange(event) { + const { windowId, setWindowViewType } = this.props; + + setWindowViewType(windowId, event.target.value); + } + + /** + * render + * + * @return {type} description + */ + render() { + const { windowViewType, t } = this.props; + + return ( + <> + <Typography>{t('view')}</Typography> + <RadioGroup aria-label={t('position')} name="position" value={windowViewType} onChange={this.handleChange} row> + <FormControlLabel + value="single" + control={<Radio color="primary" icon={<PhotoIcon />} checkedIcon={<PhotoIcon />} />} + label={t('single')} + labelPlacement="bottom" + /> + <FormControlLabel + value="book" + control={<Radio color="primary" icon={<ViewAgendaIconRotated />} checkedIcon={<ViewAgendaIconRotated />} />} + label={t('book')} + labelPlacement="bottom" + /> + </RadioGroup> + </> + ); + } +} + +/** + * @private + */ +function ViewAgendaIconRotated(props) { + return ( + <ViewAgendaIcon style={{ transform: 'rotate(-90deg)' }} /> + ); +} + +WindowViewSettings.propTypes = { + windowId: PropTypes.string.isRequired, + setWindowViewType: PropTypes.func.isRequired, + windowViewType: PropTypes.string.isRequired, + t: PropTypes.func, +}; +WindowViewSettings.defaultProps = { + t: key => key, +}; diff --git a/src/containers/WindowViewSettings.js b/src/containers/WindowViewSettings.js new file mode 100644 index 0000000000000000000000000000000000000000..0b83fe4f2257cb278fda4d1b72cb30f0ffc3cfc6 --- /dev/null +++ b/src/containers/WindowViewSettings.js @@ -0,0 +1,34 @@ +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; +import * as actions from '../state/actions'; +import { getWindowViewType } from '../state/selectors'; +import WindowViewSettings from '../components/WindowViewSettings'; + +/** + * mapDispatchToProps - used to hook up connect to action creators + * @memberof ManifestListItem + * @private + */ +const mapDispatchToProps = { setWindowViewType: actions.setWindowViewType }; + +/** + * mapStateToProps - to hook up connect + * @memberof WindowViewer + * @private + */ +const mapStateToProps = (state, props) => ( + { + windowViewType: getWindowViewType(state, props.windowId), + } +); + +const enhance = compose( + connect(mapStateToProps, mapDispatchToProps), + withNamespaces(), + miradorWithPlugins, + // further HOC go here +); + +export default enhance(WindowViewSettings); diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js index bd11ddcb9324722bc910e6fbfa1933522d952e8b..fb78df8cd37afc14c63f6fde5cce59ac1f3d9679 100644 --- a/src/state/actions/action-types.js +++ b/src/state/actions/action-types.js @@ -13,6 +13,7 @@ const ActionTypes = { RECEIVE_MANIFEST_FAILURE: 'RECEIVE_MANIFEST_FAILURE', SET_CONFIG: 'SET_CONFIG', SET_WINDOW_THUMBNAIL_POSITION: 'SET_WINDOW_THUMBNAIL_POSITION', + SET_WINDOW_VIEW_TYPE: 'SET_WINDOW_VIEW_TYPE', 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/window.js b/src/state/actions/window.js index ee50b7827755f19190dcfd8d0969b5567505d899..e4f2aafe040e1bbfe277d820658676bffc1b9073 100644 --- a/src/state/actions/window.js +++ b/src/state/actions/window.js @@ -27,6 +27,7 @@ export function addWindow(options) { thumbnailNavigationPosition: 'bottom', // bottom by default in settings.js xywh: [0, 0, 400, 400], rotation: null, + view: 'single', }; return { type: ActionTypes.ADD_WINDOW, window: { ...defaultOptions, ...options } }; } @@ -72,3 +73,14 @@ export function toggleWindowSideBarPanel(windowId, panelType) { export function setWindowThumbnailPosition(windowId, position) { return { type: ActionTypes.SET_WINDOW_THUMBNAIL_POSITION, windowId, position }; } + +/** + * setWindowViewType - action creator + * + * @param {String} windowId + * @param {String} viewType + * @memberof ActionCreators + */ +export function setWindowViewType(windowId, viewType) { + return { type: ActionTypes.SET_WINDOW_VIEW_TYPE, windowId, viewType }; +} diff --git a/src/state/reducers/windows.js b/src/state/reducers/windows.js index 108f77d2209d00d2586d673316b6ee3c64db300d..e58b42d117e7ab0540aba8865eee2eb847739bfe 100644 --- a/src/state/reducers/windows.js +++ b/src/state/reducers/windows.js @@ -22,7 +22,14 @@ const windowsReducer = (state = {}, action) => { sideBarOpen: !state[action.windowId].sideBarOpen, }, }; - + case ActionTypes.SET_WINDOW_VIEW_TYPE: + return { + ...state, + [action.windowId]: { + ...state[action.windowId], + view: action.viewType, + }, + }; case ActionTypes.SET_WINDOW_THUMBNAIL_POSITION: return { ...state, diff --git a/src/state/selectors/index.js b/src/state/selectors/index.js index 1c46754826edf65274a15be24fd347d8548db66d..36b3189cc482135b5f3691906e89d948ff6d416b 100644 --- a/src/state/selectors/index.js +++ b/src/state/selectors/index.js @@ -54,3 +54,12 @@ export function getManifestTitle(manifest) { && manifest.manifestation && manifest.manifestation.getLabel().map(label => label.value)[0]; } + +/** Return type of view in a certain window. +* @param {object} state +* @param {String} windowId +* @param {String} +*/ +export function getWindowViewType(state, windowId) { + return state.windows[windowId] && state.windows[windowId].view; +}