From 5dd569d0ff421585d4fa14b89c3b076b6c6f99f2 Mon Sep 17 00:00:00 2001
From: Chris Beer <cabeer@stanford.edu>
Date: Fri, 15 May 2020 09:37:12 -0700
Subject: [PATCH] Extract configuration for view types

---
 .../src/components/WindowViewSettings.test.js |  4 +-
 __tests__/src/selectors/windows.test.js       | 37 +++++++++
 src/components/WindowViewSettings.js          | 75 ++++++++-----------
 src/config/settings.js                        |  8 +-
 src/containers/WindowViewSettings.js          |  3 +-
 src/state/selectors/config.js                 |  5 ++
 src/state/selectors/windows.js                | 42 ++++++++---
 7 files changed, 119 insertions(+), 55 deletions(-)

diff --git a/__tests__/src/components/WindowViewSettings.test.js b/__tests__/src/components/WindowViewSettings.test.js
index b33a2e81d..bd19497b2 100644
--- a/__tests__/src/components/WindowViewSettings.test.js
+++ b/__tests__/src/components/WindowViewSettings.test.js
@@ -7,11 +7,12 @@ import { WindowViewSettings } from '../../../src/components/WindowViewSettings';
 
 /** create wrapper */
 function createWrapper(props) {
-  return shallow(
+  return mount(
     <WindowViewSettings
       classes={{}}
       windowId="xyz"
       setWindowViewType={() => {}}
+      viewTypes={['single', 'book', 'scroll', 'gallery']}
       windowViewType="single"
       {...props}
     />,
@@ -64,6 +65,7 @@ describe('WindowViewSettings', () => {
         classes={{}}
         windowId="xyz"
         setWindowViewType={() => {}}
+        viewTypes={['single', 'book', 'scroll', 'gallery']}
         windowViewType="single"
       />,
     );
diff --git a/__tests__/src/selectors/windows.test.js b/__tests__/src/selectors/windows.test.js
index 5bc269967..3db842886 100644
--- a/__tests__/src/selectors/windows.test.js
+++ b/__tests__/src/selectors/windows.test.js
@@ -11,6 +11,7 @@ import {
   getWindowManifests,
   getWindows,
   getMaximizedWindowsIds,
+  getAllowedWindowViewTypes,
 } from '../../../src/state/selectors/windows';
 
 describe('getWindows', () => {
@@ -104,6 +105,12 @@ describe('getWindowViewType', () => {
     config: {
       window: {
         defaultView: 'default',
+        views: [
+          { behaviors: ['individuals'], key: 'single' },
+          { behaviors: ['paged'], key: 'book' },
+          { behaviors: ['continuous'], key: 'scroll' },
+          { key: 'gallery' },
+        ],
       },
     },
     manifests: {
@@ -150,6 +157,36 @@ describe('getWindowViewType', () => {
   });
 });
 
+describe('getAllowedWindowViewTypes', () => {
+  const state = {
+    config: {
+      window: {
+        defaultView: 'single',
+        views: [
+          { behaviors: ['individuals'], key: 'single' },
+          { behaviors: ['paged'], key: 'book' },
+          { behaviors: ['continuous'], key: 'scroll' },
+          { key: 'gallery' },
+        ],
+      },
+    },
+    manifests: {
+      x: { json: { ...manifestFixture001 } },
+      y: { json: { ...manifestFixture015 } },
+    },
+  };
+
+  it('should return unrestricted view types', () => {
+    const received = getAllowedWindowViewTypes(state, { manifestId: 'x' });
+    expect(received).toEqual(['single', 'gallery']);
+  });
+
+  it('should return view types where behaviors match', () => {
+    const received = getAllowedWindowViewTypes(state, { manifestId: 'y' });
+    expect(received).toEqual(['single', 'book', 'gallery']);
+  });
+});
+
 describe('getViewer', () => {
   const state = {
     viewers: {
diff --git a/src/components/WindowViewSettings.js b/src/components/WindowViewSettings.js
index 92d7116a9..f8faba985 100644
--- a/src/components/WindowViewSettings.js
+++ b/src/components/WindowViewSettings.js
@@ -4,6 +4,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
 import MenuItem from '@material-ui/core/MenuItem';
 import ListSubheader from '@material-ui/core/ListSubheader';
 import SingleIcon from '@material-ui/icons/CropOriginalSharp';
+import ScrollViewIcon from '@material-ui/icons/ViewColumn';
 import PropTypes from 'prop-types';
 import BookViewIcon from './icons/BookViewIcon';
 import GalleryViewIcon from './icons/GalleryViewIcon';
@@ -55,53 +56,41 @@ export class WindowViewSettings extends Component {
    */
   render() {
     const {
-      classes, handleClose, t, windowViewType,
+      classes, handleClose, t, windowViewType, viewTypes,
     } = this.props;
 
+    if (viewTypes.length === 0) return null;
+
+    const iconMap = {
+      book: BookViewIcon,
+      gallery: GalleryViewIcon,
+      scroll: ScrollViewIcon,
+      single: SingleIcon,
+    };
+
+    /** */
+    const ViewItem = ({ Icon, value }) => (
+      <MenuItem
+        className={classes.MenuItem}
+        ref={ref => this.handleSelectedRef(ref)}
+        onClick={() => { this.handleChange(value); handleClose(); }}
+      >
+        <FormControlLabel
+          value={value}
+          classes={{ label: windowViewType === value ? classes.selectedLabel : classes.label }}
+          control={Icon && <Icon color={windowViewType === value ? 'secondary' : undefined} />}
+          label={t(value)}
+          labelPlacement="bottom"
+        />
+      </MenuItem>
+    );
+
     return (
       <>
         <ListSubheader role="presentation" disableSticky tabIndex="-1">{t('view')}</ListSubheader>
-
-        <MenuItem
-          className={classes.MenuItem}
-          ref={ref => this.handleSelectedRef(ref)}
-          onClick={() => { this.handleChange('single'); handleClose(); }}
-        >
-          <FormControlLabel
-            value="single"
-            classes={{ label: windowViewType === 'single' ? classes.selectedLabel : classes.label }}
-            control={<SingleIcon color={windowViewType === 'single' ? 'secondary' : undefined} />}
-            label={t('single')}
-            labelPlacement="bottom"
-          />
-        </MenuItem>
-        <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('book'); handleClose(); }}>
-          <FormControlLabel
-            value="book"
-            classes={{ label: windowViewType === 'book' ? classes.selectedLabel : classes.label }}
-            control={<BookViewIcon color={windowViewType === 'book' ? 'secondary' : undefined} />}
-            label={t('book')}
-            labelPlacement="bottom"
-          />
-        </MenuItem>
-        <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('scroll'); handleClose(); }}>
-          <FormControlLabel
-            value="scroll"
-            classes={{ label: windowViewType === 'scroll' ? classes.selectedLabel : classes.label }}
-            control={<BookViewIcon color={windowViewType === 'scroll' ? 'secondary' : undefined} />}
-            label={t('scroll')}
-            labelPlacement="bottom"
-          />
-        </MenuItem>
-        <MenuItem className={classes.MenuItem} onClick={() => { this.handleChange('gallery'); handleClose(); }}>
-          <FormControlLabel
-            value="gallery"
-            classes={{ label: windowViewType === 'gallery' ? classes.selectedLabel : classes.label }}
-            control={<GalleryViewIcon color={windowViewType === 'gallery' ? 'secondary' : undefined} />}
-            label={t('gallery')}
-            labelPlacement="bottom"
-          />
-        </MenuItem>
+        {viewTypes.map(value => (
+          <ViewItem Icon={iconMap[value]} key={value} value={value} />
+        ))}
       </>
     );
   }
@@ -112,10 +101,12 @@ WindowViewSettings.propTypes = {
   handleClose: PropTypes.func,
   setWindowViewType: PropTypes.func.isRequired,
   t: PropTypes.func,
+  viewTypes: PropTypes.arrayOf(PropTypes.string),
   windowId: PropTypes.string.isRequired,
   windowViewType: PropTypes.string.isRequired,
 };
 WindowViewSettings.defaultProps = {
   handleClose: () => {},
   t: key => key,
+  viewTypes: [],
 };
diff --git a/src/config/settings.js b/src/config/settings.js
index a81460c65..419a0c62f 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -242,7 +242,13 @@ export default {
       canvas: true,
       annotations: true,
       search: true,
-    }
+    },
+    views: [
+      { key: 'single', behaviors: ['individuals'] },
+      { key: 'book', behaviors: ['paged'] },
+      { key: 'scroll', behaviors: ['continuous'] },
+      { key: 'gallery' },
+    ],
   },
   windows: [ // Array of windows to be open when mirador initializes (each object should at least provide a manifestId key with the value of the IIIF presentation manifest to load)
     /**
diff --git a/src/containers/WindowViewSettings.js b/src/containers/WindowViewSettings.js
index e4b47ae70..6fc417f28 100644
--- a/src/containers/WindowViewSettings.js
+++ b/src/containers/WindowViewSettings.js
@@ -4,7 +4,7 @@ import { withTranslation } from 'react-i18next';
 import { withStyles } from '@material-ui/core/styles';
 import { withPlugins } from '../extend/withPlugins';
 import * as actions from '../state/actions';
-import { getWindowViewType } from '../state/selectors';
+import { getAllowedWindowViewTypes, getWindowViewType } from '../state/selectors';
 import { WindowViewSettings } from '../components/WindowViewSettings';
 
 /**
@@ -21,6 +21,7 @@ const mapDispatchToProps = { setWindowViewType: actions.setWindowViewType };
  */
 const mapStateToProps = (state, { windowId }) => (
   {
+    viewTypes: getAllowedWindowViewTypes(state, { windowId }),
     windowViewType: getWindowViewType(state, { windowId }),
   }
 );
diff --git a/src/state/selectors/config.js b/src/state/selectors/config.js
index 6612fceab..ed7981d2b 100644
--- a/src/state/selectors/config.js
+++ b/src/state/selectors/config.js
@@ -52,6 +52,11 @@ export const getDefaultView = createSelector(
   ({ window }) => window && window.defaultView,
 );
 
+export const getViewConfigs = createSelector(
+  [getConfig],
+  ({ window }) => (window && window.views) || [],
+);
+
 export const getThemeDirection = createSelector(
   [getConfig],
   ({ theme }) => theme.direction || 'ltr',
diff --git a/src/state/selectors/windows.js b/src/state/selectors/windows.js
index 67c490f87..c24ca52d8 100644
--- a/src/state/selectors/windows.js
+++ b/src/state/selectors/windows.js
@@ -5,7 +5,7 @@ import {
   getManifestViewingHint,
   getManifestoInstance,
 } from './manifests';
-import { getDefaultView } from './config';
+import { getDefaultView, getViewConfigs } from './config';
 import { getWorkspaceType } from './workspace';
 
 /**
@@ -85,19 +85,41 @@ export const getWindowViewType = createSelector(
     getManifestViewingHint,
     getManifestBehaviors,
     getDefaultView,
+    getViewConfigs,
   ],
-  (window, manifestViewingHint, manifestBehaviors, defaultView) => {
-    const lookup = {
-      continuous: 'scroll',
-      individuals: 'single',
-      paged: 'book',
-    };
-    return (window && window.view)
-      || lookup[manifestBehaviors.find(b => lookup[b]) || manifestViewingHint]
-      || defaultView;
+  (window, manifestViewingHint, manifestBehaviors, defaultView, viewConfig) => {
+    if (window && window.view) return window.view;
+
+    const config = viewConfig.find(view => (
+      view.behaviors
+      && view.behaviors.some(b => manifestViewingHint === b || manifestBehaviors.includes(b))
+    ));
+
+    return (config && config.key) || defaultView;
   },
 );
 
+/** */
+export const getAllowedWindowViewTypes = createSelector(
+  [
+    getManifestViewingHint,
+    getManifestBehaviors,
+    getDefaultView,
+    getViewConfigs,
+  ],
+  (manifestViewingHint, manifestBehaviors, defaultView, viewConfig) => (
+    viewConfig.reduce((allowedViews, view) => {
+      if (
+        view.key === defaultView
+        || !view.behaviors
+        || view.behaviors.some(b => (
+          manifestViewingHint === b || manifestBehaviors.includes(b)
+        ))) allowedViews.push(view.key);
+      return allowedViews;
+    }, [])
+  ),
+);
+
 export const getViewer = createSelector(
   [
     state => state.viewers,
-- 
GitLab