diff --git a/__tests__/src/actions/annotation.test.js b/__tests__/src/actions/annotation.test.js index 3609486faf6b882657a05c14d2cbed5c689a92e2..5428ce0532b4855fa4043cbd127c8929df04e779 100644 --- a/__tests__/src/actions/annotation.test.js +++ b/__tests__/src/actions/annotation.test.js @@ -133,4 +133,13 @@ describe('annotation actions', () => { }; expect(actions.deselectAnnotation(windowId, canvasId, annotationId)).toEqual(expectedAction); }); + + it('handles the toggleAnnotationDisplay action', () => { + const windowId = 'wId1'; + const expectedAction = { + type: ActionTypes.TOGGLE_ANNOTATION_DISPLAY, + windowId, + }; + expect(actions.toggleAnnotationDisplay(windowId)).toEqual(expectedAction); + }); }); diff --git a/__tests__/src/components/AnnotationSettings.test.js b/__tests__/src/components/AnnotationSettings.test.js new file mode 100644 index 0000000000000000000000000000000000000000..259453fc1f770d322ad6e34066d5178b3559eed6 --- /dev/null +++ b/__tests__/src/components/AnnotationSettings.test.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { AnnotationSettings } from '../../../src/components/AnnotationSettings'; + +/** */ +function createWrapper(props) { + return shallow( + <AnnotationSettings + displayAll={false} + displayAllDisabled={false} + t={k => k} + toggleAnnotationDisplay={() => {}} + windowId="abc123" + {...props} + />, + ); +} + +describe('AnnotationSettings', () => { + let control; + let wrapper; + const toggleAnnotationDisplayMock = jest.fn(); + + + it('renders a FormControlLabel and a Switch', () => { + wrapper = createWrapper(); + control = shallow( + wrapper.find('WithStyles(WithFormControlContext(FormControlLabel))').props().control, + ); + expect(wrapper.find('WithStyles(WithFormControlContext(FormControlLabel))').length).toBe(1); + expect(control.find('Switch').length).toBe(1); + }); + + describe('control', () => { + it('is not checked when the displayAll prop is false', () => { + wrapper = createWrapper(); + control = shallow( + wrapper.find('WithStyles(WithFormControlContext(FormControlLabel))').props().control, + ); + + expect(control.find('Switch').props().checked).toBe(false); + }); + + it('is checked when the displayAll prop is true', () => { + wrapper = createWrapper({ displayAll: true }); + control = shallow( + wrapper.find('WithStyles(WithFormControlContext(FormControlLabel))').props().control, + ); + + expect(control.find('Switch').props().checked).toBe(true); + }); + + it('is disabled based on the displayAllDisabled prop', () => { + wrapper = createWrapper(); + control = shallow( + wrapper.find('WithStyles(WithFormControlContext(FormControlLabel))').props().control, + ); + expect(control.find('Switch').props().disabled).toBe(false); + + wrapper = createWrapper({ displayAllDisabled: true }); + control = shallow( + wrapper.find('WithStyles(WithFormControlContext(FormControlLabel))').props().control, + ); + expect(control.find('Switch').props().disabled).toBe(true); + }); + + it('calls the toggleAnnotationDisplay prop function on change', () => { + wrapper = createWrapper({ toggleAnnotationDisplay: toggleAnnotationDisplayMock }); + control = shallow( + wrapper.find('WithStyles(WithFormControlContext(FormControlLabel))').props().control, + ); + + control.find('Switch').props().onChange(); // trigger the onChange prop + + expect(toggleAnnotationDisplayMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js b/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js index d403cd1c28dc9bc2336f17fa79df0d7c6b3b759a..44039692ccbcd0e7668c85398450b7f4f97aa8a6 100644 --- a/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js +++ b/__tests__/src/components/WindowSideBarAnnotationsPanel.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; +import AnnotationSettings from '../../../src/containers/AnnotationSettings'; import { WindowSideBarAnnotationsPanel } from '../../../src/components/WindowSideBarAnnotationsPanel'; /** */ @@ -27,6 +28,10 @@ describe('WindowSideBarAnnotationsPanel', () => { ).toBe('annotations'); }); + it('has the AnnotationSettings component', () => { + expect(createWrapper().find(AnnotationSettings).length).toBe(1); + }); + it('renders a list with a list item for each annotation', () => { wrapper = createWrapper({ annotations: [ diff --git a/__tests__/src/reducers/windows.test.js b/__tests__/src/reducers/windows.test.js index 800af2f76e37f51e25e441d5400dafc47537ef34..a623cd699b76e06b068b0345494ac1ab82567798 100644 --- a/__tests__/src/reducers/windows.test.js +++ b/__tests__/src/reducers/windows.test.js @@ -345,5 +345,17 @@ describe('windows reducer', () => { expect(windowsReducer(beforeState, action)).toEqual(expectedState); }); }); + + it('handles TOGGLE_ANNOTATION_DISPLAY by toggling the given window\'s displayAllAnnotation value', () => { + const beforeState = { abc123: { displayAllAnnotations: false } }; + const action = { + type: ActionTypes.TOGGLE_ANNOTATION_DISPLAY, windowId: 'abc123', + }; + const expectedState = { + abc123: { displayAllAnnotations: true }, + }; + + expect(windowsReducer(beforeState, action)).toEqual(expectedState); + }); }); }); diff --git a/__tests__/src/selectors/index.test.js b/__tests__/src/selectors/index.test.js index 67c95aa785ecdf32dd2d6090a9a674c38d2ada78..a61266579feb2afb062611a77cfb6cce7d6b4a14 100644 --- a/__tests__/src/selectors/index.test.js +++ b/__tests__/src/selectors/index.test.js @@ -1,4 +1,5 @@ import { + getAllOrSelectedAnnotations, getAnnotationResourcesByMotivation, getIdAndContentOfResources, getLanguagesFromConfigWithCurrent, @@ -166,3 +167,39 @@ it('getSelectedTargetAnnotationResources filters the annotation resources by the getSelectedTargetAnnotationResources(state, ['cid1'], ['annoId1', 'annoId2'])[0].resources.length, ).toBe(2); }); + +describe('getAllOrSelectedAnnotations', () => { + it('returns all annotations if the given window is set to display all', () => { + const state = { + windows: { + abc123: { displayAllAnnotations: true }, + }, + annotations: { + cid1: { + annoId1: { id: 'annoId1', json: { resources: [{ '@id': 'annoId1' }, { '@id': 'annoId2' }] } }, + }, + }, + }; + + expect( + getAllOrSelectedAnnotations(state, 'abc123', ['cid1'], ['annoId1'])[0].resources.length, + ).toBe(2); + }); + + it('returns only selected annotations if the window is not set to display all', () => { + const state = { + windows: { + abc123: { displayAllAnnotations: false }, + }, + annotations: { + cid1: { + annoId1: { id: 'annoId1', json: { resources: [{ '@id': 'annoId1' }, { '@id': 'annoId2' }] } }, + }, + }, + }; + + expect( + getAllOrSelectedAnnotations(state, 'abc123', ['cid1'], ['annoId1'])[0].resources.length, + ).toBe(1); + }); +}); diff --git a/src/components/AnnotationSettings.js b/src/components/AnnotationSettings.js new file mode 100644 index 0000000000000000000000000000000000000000..6d8313cc4bc6e50189c4162a8726ac06eb2fc33f --- /dev/null +++ b/src/components/AnnotationSettings.js @@ -0,0 +1,42 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Switch from '@material-ui/core/Switch'; + +/** + * AnnotationSettings is a component to handle various annotation + * display settings in the Annotation companion window +*/ +export class AnnotationSettings extends Component { + /** + * Returns the rendered component + */ + render() { + const { + displayAll, displayAllDisabled, t, toggleAnnotationDisplay, + } = this.props; + + return ( + <FormControlLabel + control={( + <Switch + checked={displayAll} + disabled={displayAllDisabled} + onChange={toggleAnnotationDisplay} + value={displayAll ? 'all' : 'select'} + /> + )} + label={t('displayAllAnnotations')} + labelPlacement="start" + /> + ); + } +} + +AnnotationSettings.propTypes = { + displayAll: PropTypes.bool.isRequired, + displayAllDisabled: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, + toggleAnnotationDisplay: PropTypes.func.isRequired, + windowId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types +}; diff --git a/src/components/WindowSideBarAnnotationsPanel.js b/src/components/WindowSideBarAnnotationsPanel.js index 33052018ab64eb9efad0919eefa4252cfe0f85a5..1b784e7987487d85c16ecc4b9b24a74e01ea77a2 100644 --- a/src/components/WindowSideBarAnnotationsPanel.js +++ b/src/components/WindowSideBarAnnotationsPanel.js @@ -4,6 +4,7 @@ import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import Typography from '@material-ui/core/Typography'; import { SanitizedHtml } from './SanitizedHtml'; +import AnnotationSettings from '../containers/AnnotationSettings'; import CompanionWindow from '../containers/CompanionWindow'; import ns from '../config/css-ns'; @@ -62,6 +63,7 @@ export class WindowSideBarAnnotationsPanel extends Component { } = this.props; return ( <CompanionWindow title={t('annotations')} paperClassName={ns('window-sidebar-annotation-panel')} windowId={windowId} id={id}> + <AnnotationSettings windowId={windowId} /> <div className={classes.section}> <Typography variant="subtitle2">{t('showingNumAnnotations', { number: annotations.length })}</Typography> </div> diff --git a/src/containers/AnnotationSettings.js b/src/containers/AnnotationSettings.js new file mode 100644 index 0000000000000000000000000000000000000000..5fbec40b2e8c34e514c2e1aa4447d81c32f0fb1a --- /dev/null +++ b/src/containers/AnnotationSettings.js @@ -0,0 +1,39 @@ +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; +import * as actions from '../state/actions'; +import { withPlugins } from '../extend'; +import { + getAnnotationResourcesByMotivation, + getSelectedTargetAnnotations, + getSelectedCanvas, +} from '../state/selectors'; +import { AnnotationSettings } from '../components/AnnotationSettings'; + +/** + * Mapping redux state to component props using connect + */ +const mapStateToProps = (state, { windowId }) => ({ + displayAll: state.windows[windowId].displayAllAnnotations, + displayAllDisabled: getAnnotationResourcesByMotivation( + getSelectedTargetAnnotations(state, (getSelectedCanvas(state, { windowId }) || {}).id), + ['oa:commenting', 'sc:painting'], + ).length < 2, +}); + +/** + * Mapping redux action dispatches to component props using connect + */ +const mapDispatchToProps = (dispatch, { windowId }) => ({ + toggleAnnotationDisplay: () => { + dispatch(actions.toggleAnnotationDisplay(windowId)); + }, +}); + +const enhance = compose( + withTranslation(), + connect(mapStateToProps, mapDispatchToProps), + withPlugins('AnnotationSettings'), +); + +export default enhance(AnnotationSettings); diff --git a/src/containers/OpenSeadragonViewer.js b/src/containers/OpenSeadragonViewer.js index d0aecea6e5f6abe235e13986d836459018a5d520..1959e3638dd03181abbfd96bd51942cb9368cd48 100644 --- a/src/containers/OpenSeadragonViewer.js +++ b/src/containers/OpenSeadragonViewer.js @@ -7,9 +7,9 @@ import { withPlugins } from '../extend'; import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer'; import * as actions from '../state/actions'; import { + getAllOrSelectedAnnotations, getCanvasLabel, getSelectedAnnotationIds, - getSelectedTargetAnnotationResources, } from '../state/selectors'; /** @@ -22,8 +22,9 @@ const mapStateToProps = ({ }, { windowId, currentCanvases }) => ({ viewer: viewers[windowId], label: getCanvasLabel({ windows, manifests }, { windowId, canvasIndex: 'selected' }), - annotations: getSelectedTargetAnnotationResources( - { annotations }, + annotations: getAllOrSelectedAnnotations( + { annotations, windows }, + windowId, currentCanvases.map(c => c.id), getSelectedAnnotationIds({ windows }, windowId, currentCanvases.map(c => c.id)), ), diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 2aa1e49e42f0abeae1f1a88c078b4338616abba6..ceac6cbdc12f1815eab761a3f19879df40925620 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -18,6 +18,7 @@ "currentItem": "Current item", "dark": "Dark theme", "dismiss": "Dismiss", + "displayAllAnnotations": "Highlight all", "downloadExport": "Download/Export workspace", "downloadExportWorkspace": "Download/Export workspace", "elastic": "Elastic", diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js index 64d801cdddf08fe0bb242dec73aa27fcdc34aede..ca23ce29de866ac35abcae61da6b47a604e6bd16 100644 --- a/src/state/actions/action-types.js +++ b/src/state/actions/action-types.js @@ -9,7 +9,7 @@ const ActionTypes = { RECEIVE_ANNOTATION_FAILURE: 'RECEIVE_ANNOTATION_FAILURE', DESELECT_ANNOTATION: 'DESELECT_ANNOTATION', SELECT_ANNOTATION: 'SELECT_ANNOTATION', - + TOGGLE_ANNOTATION_DISPLAY: 'TOGGLE_ANNOTATION_DISPLAY', FOCUS_WINDOW: 'FOCUS_WINDOW', SET_WORKSPACE_FULLSCREEN: 'SET_WORKSPACE_FULLSCREEN', diff --git a/src/state/actions/annotation.js b/src/state/actions/annotation.js index 3fd9c6a18076065fee85fccb17484bfd834293ac..bd18212796de11a22b101896b35c9c6247e42cf7 100644 --- a/src/state/actions/annotation.js +++ b/src/state/actions/annotation.js @@ -93,3 +93,15 @@ export function deselectAnnotation(windowId, canvasId, annotationId) { type: ActionTypes.DESELECT_ANNOTATION, windowId, canvasId, annotationId, }; } + +/** + * toggleAnnotationDisplay - action creator + * + * @param {String} windowId + * @memberof ActionCreators + */ +export function toggleAnnotationDisplay(windowId) { + return { + type: ActionTypes.TOGGLE_ANNOTATION_DISPLAY, windowId, + }; +} diff --git a/src/state/actions/window.js b/src/state/actions/window.js index b80dbf83905f9ee4dd4f9161e792298439e5beb0..07fe00a287d7e2c66aa81eb550da6b7fd2446f93 100644 --- a/src/state/actions/window.js +++ b/src/state/actions/window.js @@ -58,6 +58,7 @@ export function addWindow(options) { companionWindowIds: [cwDefault, cwThumbs], sideBarPanel: 'info', rotation: null, + displayAllAnnotations: false, selectedAnnotations: {}, view: 'single', maximized: false, diff --git a/src/state/reducers/windows.js b/src/state/reducers/windows.js index 8804af7ed5c20bcbfb6ead5bcd4d17a7d76d9e92..771fef401d85bf34e41388adf1201c3b32f9ec28 100644 --- a/src/state/reducers/windows.js +++ b/src/state/reducers/windows.js @@ -143,6 +143,14 @@ export const windowsReducer = (state = {}, action) => { }, }; } + case ActionTypes.TOGGLE_ANNOTATION_DISPLAY: + return { + ...state, + [action.windowId]: { + ...state[action.windowId], + displayAllAnnotations: !state[action.windowId].displayAllAnnotations, + }, + }; default: return state; } diff --git a/src/state/selectors/index.js b/src/state/selectors/index.js index 41a9aef4b377f0d63c9cd2461fa170a58e59a02d..d8362934c2e92585382ba880d9dbe5997b1122a4 100644 --- a/src/state/selectors/index.js +++ b/src/state/selectors/index.js @@ -105,3 +105,20 @@ export function getSelectedTargetAnnotationResources(state, targetIds, annotatio resources: annotation.resources.filter(r => annotationIds && annotationIds.includes(r.id)), })); } + +/** +* Return all of the given canvases annotations if the window +* is set to display all, otherwise only return selected +* @param {object} state +* @param {String} windowId +* @param {Array} targetIds +* @param {Array} annotationIds +* @return {Array} +*/ +export function getAllOrSelectedAnnotations(state, windowId, targetIds, annotationIds) { + if (state.windows[windowId].displayAllAnnotations) { + return getSelectedTargetsAnnotations(state, targetIds); + } + + return getSelectedTargetAnnotationResources(state, targetIds, annotationIds); +}