diff --git a/__tests__/fixtures/version-2/multipleSequences.json b/__tests__/fixtures/version-2/multipleSequences.json new file mode 100644 index 0000000000000000000000000000000000000000..cd0fc1a444799a591e8f7a43f82af695df285586 --- /dev/null +++ b/__tests__/fixtures/version-2/multipleSequences.json @@ -0,0 +1,118 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/manifest.json", + "@type": "sc:Manifest", + "label": "Urn\u00e4sch, Gemeindearchiv Urn\u00e4sch, Fragment", + "metadata": [ + { + "label": "Location", + "value": "Urn\u00e4sch" + } + ], + "description": [ + { + "@value": "These are two well preserved fragments of a Psalterium iuxta Hebraeos, which were probably written in the 10th century at the monastery of St. Gall, following the model of Cod. Sang. 19. In 1963 both fragments were detached from a messenger bag; they are held in the town archive of Urn\u00e4sch (Appenzell Ausserrhoden).", + "@language": "en" + } + ], + "license": "http://creativecommons.org/licenses/by-nc/4.0/", + "attribution": "e-codices - Virtual Manuscript Library of Switzerland", + "logo": "https://www.e-codices.ch/img/logo-for-iiif-manifest.png", + "service": [ + { + "@context": "https://www.w3.org/ns/webmention", + "@id": "https://www.e-codices.unifr.ch/webmention/receive", + "profile": "https://www.w3.org/ns/webmention", + "label": "e-codices Webmention Service" + } + ], + "related": "https://www.e-codices.ch/en/list/one/gau/Fragment", + "within": "https://www.e-codices.ch/en/list/gau", + "seeAlso": [ + { + "@id": "https://www.e-codices.unifr.ch/xml/tei_published/gau-Fragment_Solovey.xml", + "@format": "application/tei+xml" + } + ], + "sequences": [ + { + "@id": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/sequence/Sequence-1740.json", + "@type": "sc:Sequence", + "label": [ + { + "@value": "Standard", + "@language": "de" + } + ], + "canvases": [ + { + "@id": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/canvas/gau-Fragment_frag001a.json", + "@type": "sc:Canvas", + "label": "fragm1a", + "height": 6132, + "width": 8176, + "images": [ + { + "@id": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/annotation/gau-Fragment_frag001a.json", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/canvas/gau-Fragment_frag001a.json", + "resource": { + "@id": "https://www.e-codices.unifr.ch/loris/gau/gau-Fragment/gau-Fragment_frag001a.jp2/full/full/0/default/jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 6132, + "width": 8176, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://www.e-codices.unifr.ch/loris/gau/gau-Fragment/gau-Fragment_frag001a.jp2", + "profile": "http://iiif.io/api/image/2/level2.json" + } + } + } + ] + } + ] + }, + { + "@id": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/sequence/Sequence-1741.json", + "@type": "sc:Sequence", + "label": [ + { + "@value": "Lesefreundliche Sequenz", + "@language": "de" + } + ], + "canvases": [ + { + "@id": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/canvas/gau-Fragment_frag001a_1r.json", + "@type": "sc:Canvas", + "label": "fragm1a_1r", + "height": 8176, + "width": 6132, + "images": [ + { + "@id": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/annotation/gau-Fragment_frag001a_1r.json", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/canvas/gau-Fragment_frag001a_1r.json", + "resource": { + "@id": "https://www.e-codices.unifr.ch/loris/gau/gau-Fragment/gau-Fragment_frag001a_1r.jp2/full/full/0/default/jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 8176, + "width": 6132, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://www.e-codices.unifr.ch/loris/gau/gau-Fragment/gau-Fragment_frag001a_1r.jp2", + "profile": "http://iiif.io/api/image/2/level2.json" + } + } + } + ] + } + ] + } + ], + "structures": [] +} \ No newline at end of file diff --git a/__tests__/integration/mirador/sequence_switching.test.js b/__tests__/integration/mirador/sequence_switching.test.js new file mode 100644 index 0000000000000000000000000000000000000000..404e8dc274173538cc7eb00bbfcaac858fb6706f --- /dev/null +++ b/__tests__/integration/mirador/sequence_switching.test.js @@ -0,0 +1,35 @@ +/* global miradorInstance */ + +describe('Window Sidebar Sequence Dropdown', () => { + beforeAll(async () => { + await page.goto('http://127.0.0.1:4488/__tests__/integration/mirador/blank.html'); + + await expect(page).toClick('#addBtn'); + await expect(page).toClick('.mirador-add-resource-button'); + await expect(page).toFill('#manifestURL', 'http://localhost:4488/__tests__/fixtures/version-2/multipleSequences.json'); + await expect(page).toClick('#fetchBtn'); + + await expect(page).toMatchElement('[data-manifestid="http://localhost:4488/__tests__/fixtures/version-2/multipleSequences.json"] button'); + await expect(page).toClick('[data-manifestid="http://localhost:4488/__tests__/fixtures/version-2/multipleSequences.json"] button'); + }); + + it('allows the user to switch the sequence', async () => { + const windows = await page.evaluate(() => ( + miradorInstance.store.getState().windows + )); + + const windowId = Object.values(windows) + .find(window => window.manifestId === 'http://localhost:4488/__tests__/fixtures/version-2/multipleSequences.json') + .id; + + await expect(page).toMatchElement(`#${windowId} button[aria-label="Toggle sidebar"]`); + await expect(page).toClick(`#${windowId} button[aria-label="Toggle sidebar"]`); + + await expect(page).toMatchElement(`#${windowId} button[aria-label="Index"]`); + await expect(page).toClick(`#${windowId} button[aria-label="Index"]`); + await expect(page).toClick('#mui-component-select-sequenceId'); + await expect(page).toMatchElement('#sequence-1'); + await expect(page).toClick('#sequence-1'); + await expect(page).toMatchElement('p', { text: 'fragm1a_1r' }); + }); +}); diff --git a/__tests__/src/components/WindowSideBarCanvasPanel.test.js b/__tests__/src/components/WindowSideBarCanvasPanel.test.js index 05ee999a48995d3eda07bc1a30853a3979ed9868..3d2c96ea5022ea470b3c80b61283d83dd5bd57fe 100644 --- a/__tests__/src/components/WindowSideBarCanvasPanel.test.js +++ b/__tests__/src/components/WindowSideBarCanvasPanel.test.js @@ -12,6 +12,14 @@ import manifestJson from '../../fixtures/version-2/019.json'; */ function createWrapper(props) { const canvases = Utils.parseManifest(manifestJson).getSequences()[0].getCanvases(); + let sequences; + + if (props.multipleSequences) { + sequences = [{ id: 'a', label: 'seq1' }, + { id: 'b', label: 'seq2' }]; + } else { + sequences = Utils.parseManifest(manifestJson).getSequences(); + } return shallow( <WindowSideBarCanvasPanel @@ -24,6 +32,7 @@ function createWrapper(props) { config={{ canvasNavigation: { height: 100 } }} updateVariant={() => {}} selectedCanvases={[canvases[1]]} + sequences={sequences} variant="item" {...props} />, @@ -32,23 +41,37 @@ function createWrapper(props) { describe('WindowSideBarCanvasPanel', () => { it('renders SidebarIndexList', () => { - const wrapper = createWrapper(); + const wrapper = createWrapper({ multipleSequences: false }); expect(wrapper.find(CompanionWindow).props().title).toBe('canvasIndex'); expect(wrapper.find(SidebarIndexList).length).toBe(1); }); it('without a treeStructure will not render the table of contents tab', () => { - const wrapper = createWrapper(); + const wrapper = createWrapper({ multipleSequences: false }); expect( compact(wrapper.find(CompanionWindow).props().titleControls.props.children) .length, ).toBe(2); }); + it('renders form control when multiple sequences present', () => { + const wrapper = createWrapper({ multipleSequences: true }); + + expect(wrapper.find(CompanionWindow).props().titleControls.props.children[0] + .type.displayName.includes('FormControl')).toBe(true); + }); + + it('renders correct number of sequences in form control', () => { + const wrapper = createWrapper({ multipleSequences: true }); + + expect(wrapper.find(CompanionWindow).props().titleControls.props.children[0] + .props.children.props.children.length).toBe(2); + }); + describe('handleVariantChange', () => { it('updates the variant', () => { const updateVariant = jest.fn(); - const wrapper = createWrapper({ updateVariant }); + const wrapper = createWrapper({ multipleSequences: false, updateVariant }); wrapper.instance().handleVariantChange({}, 'item'); expect(updateVariant).toHaveBeenCalledWith('item'); }); diff --git a/src/components/WindowSideBarCanvasPanel.js b/src/components/WindowSideBarCanvasPanel.js index bc8297dc4a2d448495590493ad9e9bbdabfa97f5..372b4589219b0245081ab54be417f3c488ef33a4 100644 --- a/src/components/WindowSideBarCanvasPanel.js +++ b/src/components/WindowSideBarCanvasPanel.js @@ -10,6 +10,9 @@ import TocIcon from '@material-ui/icons/SortSharp'; import ThumbnailListIcon from '@material-ui/icons/ViewListSharp'; import Typography from '@material-ui/core/Typography'; import ArrowForwardIcon from '@material-ui/icons/ArrowForwardSharp'; +import FormControl from '@material-ui/core/FormControl'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; import CompanionWindow from '../containers/CompanionWindow'; import SidebarIndexList from '../containers/SidebarIndexList'; import SidebarIndexTableOfContents from '../containers/SidebarIndexTableOfContents'; @@ -21,8 +24,13 @@ export class WindowSideBarCanvasPanel extends Component { /** */ constructor(props) { super(props); + this.handleSequenceChange = this.handleSequenceChange.bind(this); this.handleVariantChange = this.handleVariantChange.bind(this); + this.state = { + sequenceSelectionOpened: false, + }; + this.containerRef = React.createRef(); } @@ -35,6 +43,18 @@ export class WindowSideBarCanvasPanel extends Component { : resource.id; } + /** @private */ + async handleSequenceChange(event) { + const { setCanvas, updateSequence } = this.props; + await updateSequence(event.target.value); + + const { windowId, canvases } = this.props; + const firstCanvasId = canvases[0].id; + setCanvas(windowId, firstCanvasId); + + this.setState({ sequenceSelectionOpened: false }); + } + /** @private */ handleVariantChange(event, value) { const { updateVariant } = this.props; @@ -47,16 +67,21 @@ export class WindowSideBarCanvasPanel extends Component { */ render() { const { + canvases, classes, collection, id, showMultipart, + sequenceId, + sequences, t, + toggleDraggingEnabled, variant, showToc, windowId, } = this.props; + const { sequenceSelectionOpened } = this.state; let listComponent; if (variant === 'tableOfContents') { @@ -84,19 +109,54 @@ export class WindowSideBarCanvasPanel extends Component { id={id} windowId={windowId} titleControls={( - <Tabs - value={variant} - onChange={this.handleVariantChange} - variant="fullWidth" - indicatorColor="primary" - textColor="primary" - > - {showToc && ( - <Tooltip title={t('tableOfContentsList')} value="tableOfContents"><Tab className={classes.variantTab} value="tableOfContents" aria-label={t('tableOfContentsList')} aria-controls={`tab-panel-${id}`} icon={<TocIcon style={{ transform: 'scale(-1, 1)' }} />} /></Tooltip> - )} - <Tooltip title={t('itemList')} value="item"><Tab className={classes.variantTab} value="item" aria-label={t('itemList')} aria-controls={`tab-panel-${id}`} icon={<ItemListIcon />} /></Tooltip> - <Tooltip title={t('thumbnailList')} value="thumbnail"><Tab className={classes.variantTab} value="thumbnail" aria-label={t('thumbnailList')} aria-controls={`tab-panel-${id}`} icon={<ThumbnailListIcon />} /></Tooltip> - </Tabs> + <> + { + sequences && sequences.length > 1 && ( + <FormControl> + <Select + MenuProps={{ + anchorOrigin: { + horizontal: 'left', + vertical: 'bottom', + }, + getContentAnchorEl: null, + }} + displayEmpty + value={sequenceId} + onChange={this.handleSequenceChange} + name="sequenceId" + open={sequenceSelectionOpened} + onOpen={(e) => { + toggleDraggingEnabled(); + this.setState({ sequenceSelectionOpened: true }); + }} + onClose={(e) => { + toggleDraggingEnabled(); + this.setState({ sequenceSelectionOpened: false }); + }} + classes={{ select: classes.select }} + className={classes.selectEmpty} + > + { sequences.map((s, i) => <MenuItem id={`sequence-${i}`} value={s.id} key={s.id}><Typography variant="body2">{ WindowSideBarCanvasPanel.getUseableLabel(s, i) }</Typography></MenuItem>) } + </Select> + </FormControl> + ) + } + <div className={classes.break} /> + <Tabs + value={variant} + onChange={this.handleVariantChange} + variant="fullWidth" + indicatorColor="primary" + textColor="primary" + > + {showToc && ( + <Tooltip title={t('tableOfContentsList')} value="tableOfContents"><Tab className={classes.variantTab} value="tableOfContents" aria-label={t('tableOfContentsList')} aria-controls={`tab-panel-${id}`} icon={<TocIcon style={{ transform: 'scale(-1, 1)' }} />} /></Tooltip> + )} + <Tooltip title={t('itemList')} value="item"><Tab className={classes.variantTab} value="item" aria-label={t('itemList')} aria-controls={`tab-panel-${id}`} icon={<ItemListIcon />} /></Tooltip> + <Tooltip title={t('thumbnailList')} value="thumbnail"><Tab className={classes.variantTab} value="thumbnail" aria-label={t('thumbnailList')} aria-controls={`tab-panel-${id}`} icon={<ThumbnailListIcon />} /></Tooltip> + </Tabs> + </> )} > <div id={`tab-panel-${id}`}> @@ -120,18 +180,27 @@ export class WindowSideBarCanvasPanel extends Component { } WindowSideBarCanvasPanel.propTypes = { + canvases: PropTypes.arrayOf(PropTypes.object), classes: PropTypes.objectOf(PropTypes.string).isRequired, collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types id: PropTypes.string.isRequired, + sequenceId: PropTypes.string, + sequences: PropTypes.arrayOf(PropTypes.object), + setCanvas: PropTypes.func.isRequired, showMultipart: PropTypes.func.isRequired, showToc: PropTypes.bool, t: PropTypes.func.isRequired, + toggleDraggingEnabled: PropTypes.func.isRequired, + updateSequence: PropTypes.func.isRequired, updateVariant: PropTypes.func.isRequired, variant: PropTypes.oneOf(['item', 'thumbnail', 'tableOfContents']).isRequired, windowId: PropTypes.string.isRequired, }; WindowSideBarCanvasPanel.defaultProps = { + canvases: [], collection: null, + sequenceId: null, + sequences: [], showToc: false, }; diff --git a/src/containers/WindowSideBarCanvasPanel.js b/src/containers/WindowSideBarCanvasPanel.js index e26303a543965736c65bfbfb8cfe203d5f466cad..a64db487b1c23f4fd580169241e88e5c18a18532 100644 --- a/src/containers/WindowSideBarCanvasPanel.js +++ b/src/containers/WindowSideBarCanvasPanel.js @@ -6,11 +6,14 @@ import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { WindowSideBarCanvasPanel } from '../components/WindowSideBarCanvasPanel'; import { + getCanvases, getCompanionWindow, getDefaultSidebarVariant, getSequenceTreeStructure, getWindow, getManifestoInstance, + getSequence, + getSequences, } from '../state/selectors'; /** @@ -24,11 +27,13 @@ const mapStateToProps = (state, { id, windowId }) => { const collectionPath = window.collectionPath || []; const collectionId = collectionPath && collectionPath[collectionPath.length - 1]; return { + canvases: getCanvases(state, { windowId }), collection: collectionId && getManifestoInstance(state, { manifestId: collectionId }), config, + sequenceId: getSequence(state, { windowId }).id, + sequences: getSequences(state, { windowId }), showToc: treeStructure && treeStructure.nodes && treeStructure.nodes.length > 0, - variant: companionWindow.variant - || getDefaultSidebarVariant(state, { windowId }), + variant: companionWindow.variant || getDefaultSidebarVariant(state, { windowId }), }; }; @@ -43,6 +48,9 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ actions.addOrUpdateCompanionWindow(windowId, { content: 'collection', position: 'right' }), ), toggleDraggingEnabled: () => dispatch(actions.toggleDraggingEnabled()), + updateSequence: sequenceId => dispatch( + actions.updateWindow(windowId, { sequenceId }), + ), updateVariant: variant => dispatch( actions.updateCompanionWindow(windowId, id, { variant }), ), @@ -53,6 +61,10 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ * @param theme */ const styles = theme => ({ + break: { + flexBasis: '100%', + height: 0, + }, collectionNavigationButton: { textTransform: 'none', },