diff --git a/__tests__/fixtures/version-2/collection.json b/__tests__/fixtures/version-2/collection.json new file mode 100644 index 0000000000000000000000000000000000000000..3ad3559df4581cc7d2cf1a3e9fdcff5651339825 --- /dev/null +++ b/__tests__/fixtures/version-2/collection.json @@ -0,0 +1,292 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/collection.json", + "@type": "sc:Collection", + "label": "Collection of Test Cases", + "manifests": [ + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/1/manifest.json", + "@type": "sc:Manifest", + "label": "Test 1 Manifest: Minimum Required Fields" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/2/manifest.json", + "@type": "sc:Manifest", + "label": "Test 2 Manifest: Metadata Pairs" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/3/manifest.json", + "@type": "sc:Manifest", + "label": "Test 3 Manifest: Metadata Pairs with Languages" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/4/manifest.json", + "@type": "sc:Manifest", + "label": "Test 4 Manifest: Metadata Pairs with Multiple Values in same Language" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/5/manifest.json", + "@type": "sc:Manifest", + "label": "Test 5 Manifest: Description field" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/6/manifest.json", + "@type": "sc:Manifest", + "label": "Test 6 Manifest: Multiple Descriptions" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/7/manifest.json", + "@type": "sc:Manifest", + "label": "Test 7 Manifest: Rights Metadata" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/8/manifest.json", + "@type": "sc:Manifest", + "label": "Test 8 Manifest: SeeAlso link / Manifest" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/9/manifest.json", + "@type": "sc:Manifest", + "label": "Test 9 Manifest: Service link / Manifest" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/10/manifest.json", + "@type": "sc:Manifest", + "label": "Test 10 Manifest: Service link as Object" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/11/manifest.json", + "@type": "sc:Manifest", + "label": "Test 11 Manifest: ViewingDirection: l-t-r" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/12/manifest.json", + "@type": "sc:Manifest", + "label": "Test 12 Manifest: ViewingDirection: r-t-l" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/13/manifest.json", + "@type": "sc:Manifest", + "label": "Test 13 Manifest: ViewingDirection: t-t-b" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/14/manifest.json", + "@type": "sc:Manifest", + "label": "Test 14 Manifest: ViewingDirection: b-t-t" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/15/manifest.json", + "@type": "sc:Manifest", + "label": "Test 15 Manifest: ViewingHint: paged" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/16/manifest.json", + "@type": "sc:Manifest", + "label": "Test 16 Manifest: ViewingHint: continuous" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/17/manifest.json", + "@type": "sc:Manifest", + "label": "Test 17 Manifest: ViewingHint: individuals" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/18/manifest.json", + "@type": "sc:Manifest", + "label": "Test 18 Manifest: Non Standard Keys" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/19/manifest.json", + "@type": "sc:Manifest", + "label": "Test 19 Manifest: Multiple Canvases" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/20/manifest.json", + "@type": "sc:Manifest", + "label": "Test 20 Manifest: Multiple Sequences" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/21/manifest.json", + "@type": "sc:Manifest", + "label": "Test 21 Manifest: Sequence with Metadata" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/22/manifest.json", + "@type": "sc:Manifest", + "label": "Test 22 Manifest: /Sequence/ with non l-t-r viewingDirection" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/23/manifest.json", + "@type": "sc:Manifest", + "label": "Test 23 Manifest: /Sequence/ with non paged viewingHint" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/24/manifest.json", + "@type": "sc:Manifest", + "label": "Test 24 Manifest: Image with IIIF Service" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/25/manifest.json", + "@type": "sc:Manifest", + "label": "Test 25 Manifest: Image with IIIF Service, embedded info" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/26/manifest.json", + "@type": "sc:Manifest", + "label": "Test 26 Manifest: Image different size to Canvas" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/27/manifest.json", + "@type": "sc:Manifest", + "label": "Test 27 Manifest: No Image" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/28/manifest.json", + "@type": "sc:Manifest", + "label": "Test 28 Manifest: Choice of Image" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/29/manifest.json", + "@type": "sc:Manifest", + "label": "Test 29 Manifest: Choice of Image with IIIF Service" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/30/manifest.json", + "@type": "sc:Manifest", + "label": "Test 30 Manifest: Main + Detail Image" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/31/manifest.json", + "@type": "sc:Manifest", + "label": "Test 31 Manifest: Detail with IIIF Service" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/32/manifest.json", + "@type": "sc:Manifest", + "label": "Test 32 Manifest: Multiple Detail Images" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/33/manifest.json", + "@type": "sc:Manifest", + "label": "Test 33 Manifest: Detail Image with Choice" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/34/manifest.json", + "@type": "sc:Manifest", + "label": "Test 34 Manifest: Detail Image with Choice, and 'no image' as option" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/35/manifest.json", + "@type": "sc:Manifest", + "label": "Test 35 Manifest: Partial Image as Main Image" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/36/manifest.json", + "@type": "sc:Manifest", + "label": "Test 36 Manifest: Partial Image as Main Image with IIIF Service" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/37/manifest.json", + "@type": "sc:Manifest", + "label": "Test 37 Manifest: Partial Image as Detail Image" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/38/manifest.json", + "@type": "sc:Manifest", + "label": "Test 38 Manifest: Partial Image as Detail Image with IIIF Service" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/39/manifest.json", + "@type": "sc:Manifest", + "label": "Test 39 Manifest: Image with CSS Rotation" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/40/manifest.json", + "@type": "sc:Manifest", + "label": "Test 40 Manifest: Multiple Languages for Metadata Labels" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/41/manifest.json", + "@type": "sc:Manifest", + "label": "Test 41 Manifest: Main Image with Server side Rotation" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/43/manifest.json", + "@type": "sc:Manifest", + "label": "Test 43 Manifest: Embedded Transcription on Canvas" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/44/manifest.json", + "@type": "sc:Manifest", + "label": "Test 44 Manifest: Embedded Transcription on Fragment Segment" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/45/manifest.json", + "@type": "sc:Manifest", + "label": "Test 45 Manifest: External text/plain Transcription on Canvas" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/46/manifest.json", + "@type": "sc:Manifest", + "label": "Test 46 Manifest: External text/plain Transcription on Segment" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/47/manifest.json", + "@type": "sc:Manifest", + "label": "Test 47 Manifest: Embedded HTML Transcription on Canvas" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/48/manifest.json", + "@type": "sc:Manifest", + "label": "Test 48 Manifest: Embedded HTML Transcription on Segment" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/51/manifest.json", + "@type": "sc:Manifest", + "label": "Test 51 Manifest: Embedded Comment on a Canvas" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/52/manifest.json", + "@type": "sc:Manifest", + "label": "Test 52 Manifest: Embedded Comment on a Segment" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/54/manifest.json", + "@type": "sc:Manifest", + "label": "Test 54 Manifest: Comment in HTML" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/61/manifest.json", + "@type": "sc:Manifest", + "label": "Test 61 Manifest: Embedded Transcription on Selector Segment" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/62/manifest.json", + "@type": "sc:Manifest", + "label": [ + { + "@value": "62: quelque titre", + "@language": "fr" + }, + { + "@value": "62: some title", + "@language": "en" + } + ] + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/63/manifest.json", + "@type": "sc:Manifest", + "label": "Test 63 Manifest: Description in Multiple Languages" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/64/manifest.json", + "@type": "sc:Manifest", + "label": "Test 64 Manifest: Description in HTML" + }, + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/65/manifest.json", + "@type": "sc:Manifest", + "label": "Test 65 Manifest: Sequence with startCanvas" + } + ] +} diff --git a/__tests__/integration/mirador/collections.html b/__tests__/integration/mirador/collections.html new file mode 100644 index 0000000000000000000000000000000000000000..8c29d6cb5e02a45792f9aa7f816e494a55948f79 --- /dev/null +++ b/__tests__/integration/mirador/collections.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="theme-color" content="#000000"> + <title>Mirador</title> + <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"> + </head> + <body> + <div id="mirador" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;"></div> + <script>document.write("<script type='text/javascript' src='../../../dist/mirador.min.js?v=" + Date.now() + "'><\/script>");</script> + <script type="text/javascript"> + var miradorInstance = Mirador.viewer({ + id: 'mirador', + windows: [{ + collectionPath: [ + "https://www.e-codices.unifr.ch/metadata/iiif/collection.json", + "https://www.e-codices.unifr.ch/metadata/iiif/collection/stabs.json" + ], + manifestId: "https://www.e-codices.unifr.ch/metadata/iiif/stabs-StAlban-DD1-1580/manifest.json" + }], + catalog: [ + { manifestId: "https://www.e-codices.unifr.ch/metadata/iiif/collection.json" }, + ] + }); + </script> + </body> +</html> diff --git a/__tests__/src/components/CollectionDialog.test.js b/__tests__/src/components/CollectionDialog.test.js new file mode 100644 index 0000000000000000000000000000000000000000..64928539f0aa225405a7bae2e02aadba52dbc90d --- /dev/null +++ b/__tests__/src/components/CollectionDialog.test.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import Button from '@material-ui/core/Button'; +import MenuItem from '@material-ui/core/MenuItem'; +import Skeleton from '@material-ui/lab/Skeleton'; +import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { CollectionDialog } from '../../../src/components/CollectionDialog'; +import collection from '../../fixtures/version-2/collection.json'; + +/** */ +function createWrapper(props) { + const manifest = Utils.parseManifest(props.manifest ? props.manifest : collection); + return shallow( + <CollectionDialog + addWindow={() => {}} + classes={{}} + ready + manifest={manifest} + t={(key) => key} + {...props} + />, + ); +} + +describe('CollectionDialog', () => { + it('renders a dialog with collection menu items', () => { + const wrapper = createWrapper({}); + expect(wrapper.find(Dialog).length).toEqual(1); + expect(wrapper.find(MenuItem).length).toEqual(55); + expect(wrapper.find(MenuItem).first().text()).toEqual('Test 1 Manifest: Minimum Required Fields'); + }); + it('when not ready returns placeholder skeleton', () => { + const wrapper = createWrapper({ ready: false }); + expect(wrapper.find(Skeleton).length).toEqual(3); + }); + it('clicking the hide button fires hideCollectionDialog', () => { + const hideCollectionDialog = jest.fn(); + const wrapper = createWrapper({ hideCollectionDialog }); + expect(wrapper.find(DialogActions).find(Button).first().simulate('click')); + expect(hideCollectionDialog).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/src/components/CollectionInfo.test.js b/__tests__/src/components/CollectionInfo.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b990a49f7213b10ba4f5f65d302081c81e2d9329 --- /dev/null +++ b/__tests__/src/components/CollectionInfo.test.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Button from '@material-ui/core/Button'; +import { CollectionInfo } from '../../../src/components/CollectionInfo'; +import CollapsibleSection from '../../../src/containers/CollapsibleSection'; + +/** */ +function createWrapper(props) { + return shallow( + <CollectionInfo + id="test" + collectionPath={[1, 2]} + showCollectionDialog={() => {}} + {...props} + />, + ); +} + +describe('CollectionInfo', () => { + it('renders a collapsible section', () => { + const wrapper = createWrapper(); + expect(wrapper.find(CollapsibleSection).length).toEqual(1); + }); + it('without a collectionPath, renders nothing', () => { + const wrapper = createWrapper({ collectionPath: [] }); + expect(wrapper.find(CollapsibleSection).length).toEqual(0); + }); + it('clicking the button fires showCollectionDialog', () => { + const showCollectionDialog = jest.fn(); + const wrapper = createWrapper({ showCollectionDialog }); + expect(wrapper.find(Button).first().simulate('click')); + expect(showCollectionDialog).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/src/components/ManifestListItem.test.js b/__tests__/src/components/ManifestListItem.test.js index 9f6d27f36b5a9f4f12609d2c1573e27ec28f75c5..72f183c722c5475f662bbb502e64e50934080eca 100644 --- a/__tests__/src/components/ManifestListItem.test.js +++ b/__tests__/src/components/ManifestListItem.test.js @@ -67,4 +67,16 @@ describe('ManifestListItem', () => { const wrapper = createWrapper(); expect(wrapper.find('.mirador-manifest-list-item-provider').children().text()).toEqual('addedFromUrl'); }); + + it('when clicking a collection fires the showCollectionDialog', () => { + const showCollectionDialog = jest.fn(); + const wrapper = createWrapper({ isCollection: true, showCollectionDialog }); + wrapper.find(ButtonBase).simulate('click'); + expect(showCollectionDialog).toHaveBeenCalledTimes(1); + }); + + it('displays a collection label for collections', () => { + const wrapper = createWrapper({ isCollection: true }); + expect(wrapper.text()).toContain('collectionxyz'); + }); }); diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js index 5d379cd7972caee138e3385e7c2051fa480d8090..04107ca69dd4c5ef8910b4704128d6ba46e40bce 100644 --- a/__tests__/src/lib/MiradorViewer.test.js +++ b/__tests__/src/lib/MiradorViewer.test.js @@ -58,10 +58,10 @@ describe('MiradorViewer', () => { expect(windows[windowIds[0]].view).toBe(undefined); expect(windows[windowIds[1]].view).toBe('book'); - expect(catalog.length).toBe(1); - expect(catalog[0].manifestId).toBe('http://media.nga.gov/public/manifests/nga_highlights.json'); - expect(catalog[0].provider).toBe('National Gallery of Art'); - + expect(catalog.length).toBe(2); + expect(catalog[0].manifestId).toBe('https://iiif.harvardartmuseums.org/manifests/object/299843'); + expect(catalog[1].manifestId).toBe('http://media.nga.gov/public/manifests/nga_highlights.json'); + expect(catalog[1].provider).toBe('National Gallery of Art'); expect(config.foo).toBe('bar'); }); it('merges translation configs from multiple plugins', () => { diff --git a/__tests__/src/reducers/catalog.test.js b/__tests__/src/reducers/catalog.test.js index edd4db6dd6e5b7facbd39e0007e24a38b234fb95..b232590e94f29e8ef231f7c688ad63a6965908e2 100644 --- a/__tests__/src/reducers/catalog.test.js +++ b/__tests__/src/reducers/catalog.test.js @@ -39,19 +39,19 @@ describe('catalog reducer', () => { }); }); - describe('REQUEST_MANIFEST', () => { + describe('ADD_WINDOW', () => { it('adds new manifests to the state', () => { expect(catalogReducer([], { - manifestId: '1', - type: ActionTypes.REQUEST_MANIFEST, + type: ActionTypes.ADD_WINDOW, + window: { manifestId: '1' }, })).toEqual([ { manifestId: '1' }, ]); }); it('adds new manifests to the top of state', () => { expect(catalogReducer([{ manifestId: '2' }], { - manifestId: '1', - type: ActionTypes.REQUEST_MANIFEST, + type: ActionTypes.ADD_WINDOW, + window: { manifestId: '1' }, })).toEqual([ { manifestId: '1' }, { manifestId: '2' }, @@ -59,8 +59,8 @@ describe('catalog reducer', () => { }); it('deduplicate manifests', () => { expect(catalogReducer([{ manifestId: '1' }], { - manifestId: '1', - type: ActionTypes.REQUEST_MANIFEST, + type: ActionTypes.ADD_WINDOW, + window: { manifestId: '1' }, })).toEqual([ { manifestId: '1' }, ]); diff --git a/__tests__/src/sagas/app.test.js b/__tests__/src/sagas/app.test.js index ce4467d0939cee103190f468ddb0deaba5f3e2d5..193f21cdb694cc3ed529f8e45c2cdec03a994352 100644 --- a/__tests__/src/sagas/app.test.js +++ b/__tests__/src/sagas/app.test.js @@ -2,7 +2,7 @@ import { call } from 'redux-saga/effects'; import { expectSaga, testSaga } from 'redux-saga-test-plan'; import { importConfig, importState } from '../../../src/state/sagas/app'; -import { fetchManifest } from '../../../src/state/sagas/iiif'; +import { fetchManifests } from '../../../src/state/sagas/iiif'; import { fetchWindowManifest } from '../../../src/state/sagas/windows'; import { addWindow } from '../../../src/state/actions'; @@ -26,7 +26,7 @@ describe('app-level sagas', () => { call(fetchWindowManifest, { id: 'y', payload: { id: 'y', manifestId: 'url2' } }), ]); }); - it('calls into fetchManifest for each manifest', () => { + it('calls into fetchManifests for each manifest', () => { const action = { state: { manifests: { x: { id: 'x' } }, @@ -37,7 +37,7 @@ describe('app-level sagas', () => { testSaga(importState, action) .next() .all([ - call(fetchManifest, { manifestId: 'x' }), + call(fetchManifests, 'x'), ]); }); it('does not fetchManifest if the manifest json was provided', () => { diff --git a/__tests__/src/sagas/windows.test.js b/__tests__/src/sagas/windows.test.js index f8a696388fe75184a133279333f48a23ace68934..e900713f12b804e17e8455d33c2b08b41c953f92 100644 --- a/__tests__/src/sagas/windows.test.js +++ b/__tests__/src/sagas/windows.test.js @@ -3,7 +3,7 @@ import { expectSaga } from 'redux-saga-test-plan'; import { Utils } from 'manifesto.js/dist-esmodule/Utils'; import ActionTypes from '../../../src/state/actions/action-types'; -import { receiveManifest, setCanvas } from '../../../src/state/actions'; +import { setCanvas } from '../../../src/state/actions'; import { getManifests, getManifestoInstance, getManifestSearchService, getCompanionWindowIdsForPosition, @@ -15,7 +15,7 @@ import { getVisibleCanvasIds, getCanvasForAnnotation, getCanvases, selectInfoResponses, } from '../../../src/state/selectors'; -import { fetchManifest } from '../../../src/state/sagas/iiif'; +import { fetchManifests } from '../../../src/state/sagas/iiif'; import { fetchWindowManifest, setWindowDefaultSearchQuery, @@ -27,12 +27,13 @@ import { panToFocusedWindow, setCurrentAnnotationsOnCurrentCanvas, fetchInfoResponses, + setCollectionPath, } from '../../../src/state/sagas/windows'; import fixture from '../../fixtures/version-2/019.json'; describe('window-level sagas', () => { describe('fetchWindowManifest', () => { - it('calls into fetchManifest for each window', () => { + it('calls into fetchManifests for each window', () => { const action = { window: { id: 'x', @@ -43,48 +44,12 @@ describe('window-level sagas', () => { return expectSaga(fetchWindowManifest, action) .provide([ [select(getManifests), {}], - [call(fetchManifest, { manifestId: 'manifest.json' }), {}], + [call(fetchManifests, 'manifest.json'), {}], [call(setWindowStartingCanvas, action)], [call(setWindowDefaultSearchQuery, action)], + [call(setCollectionPath, { manifestId: 'manifest.json', windowId: 'x' })], ]) - .call(fetchManifest, { manifestId: 'manifest.json' }) - .run(); - }); - - it('calls retrieveManifest if a manifest was provided', () => { - const manifestJson = { data: '123' }; - const action = { - manifest: manifestJson, - window: { - id: 'x', - manifestId: 'manifest.json', - }, - }; - - return expectSaga(fetchWindowManifest, action) - .provide([ - [call(setWindowStartingCanvas, action)], - [call(setWindowDefaultSearchQuery, action)], - ]) - .put(receiveManifest('manifest.json', manifestJson)) - .run(); - }); - - it('does not call fetchManifest if the manifest is already available', () => { - const action = { - window: { - id: 'x', - manifestId: 'manifest.json', - }, - }; - - return expectSaga(fetchWindowManifest, action) - .provide([ - [select(getManifests), { 'manifest.json': {} }], - [call(setWindowStartingCanvas, action)], - [call(setWindowDefaultSearchQuery, action)], - ]) - .not.call(fetchManifest, { manifestId: 'manifest.json' }) + .call(fetchManifests, 'manifest.json') .run(); }); it('calls additional methods after ensuring we have a manifest', () => { @@ -100,6 +65,7 @@ describe('window-level sagas', () => { [select(getManifests), { 'manifest.json': {} }], [call(setWindowStartingCanvas, action)], [call(setWindowDefaultSearchQuery, action)], + [call(setCollectionPath, { manifestId: 'manifest.json', windowId: 'x' })], ]) .call(setWindowStartingCanvas, action) .call(setWindowDefaultSearchQuery, action) @@ -117,7 +83,7 @@ describe('window-level sagas', () => { }, }; - return expectSaga(fetchWindowManifest, action) + return expectSaga(setWindowStartingCanvas, action) .provide([ [select(getManifests), { 'manifest.json': {} }], [call(setCanvas, 'x', '1', null, { preserveViewport: false }), { type: 'setCanvasThunk' }], @@ -135,7 +101,7 @@ describe('window-level sagas', () => { }, }; - return expectSaga(fetchWindowManifest, action) + return expectSaga(setWindowStartingCanvas, action) .provide([ [select(getManifests), { 'manifest.json': {} }], [call(setCanvas, 'x', '1', null, { preserveViewport: true }), { type: 'setCanvasThunk' }], diff --git a/src/components/CollectionDialog.js b/src/components/CollectionDialog.js new file mode 100644 index 0000000000000000000000000000000000000000..59730884c4e6500cf80b40a554067096a217c909 --- /dev/null +++ b/src/components/CollectionDialog.js @@ -0,0 +1,267 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { + Button, + Chip, + Dialog, + DialogActions, + DialogTitle, + Link, + MenuList, + MenuItem, + Typography, +} from '@material-ui/core'; +import ArrowBackIcon from '@material-ui/icons/ArrowBackSharp'; +import Skeleton from '@material-ui/lab/Skeleton'; +import { LabelValueMetadata } from './LabelValueMetadata'; +import CollapsibleSection from '../containers/CollapsibleSection'; +import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent'; +import ManifestInfo from '../containers/ManifestInfo'; + +/** + */ +function asArray(value) { + if (!Array.isArray(value)) { + return [value]; + } + + return value; +} + +/** + * a simple dialog providing the possibility to switch the theme + */ +export class CollectionDialog extends Component { + /** */ + static getUseableLabel(resource, index) { + return (resource + && resource.getLabel + && resource.getLabel().length > 0) + ? resource.getLabel().map(label => label.value)[0] + : String(index + 1); + } + + /** */ + constructor(props) { + super(props); + + this.state = { filter: null }; + } + + /** */ + setFilter(filter) { + this.setState({ filter }); + } + + /** */ + selectCollection(c) { + const { + collectionPath, + manifestId, + showCollectionDialog, + windowId, + } = this.props; + + showCollectionDialog(c.id, [...collectionPath, manifestId], windowId); + } + + /** */ + goToPreviousCollection() { + const { collectionPath, showCollectionDialog, windowId } = this.props; + + showCollectionDialog( + collectionPath[collectionPath.length - 1], + collectionPath.slice(0, -1), + windowId, + ); + } + + /** */ + selectManifest(m) { + const { + addWindow, + collectionPath, + hideCollectionDialog, + manifestId, + setWorkspaceAddVisibility, + updateWindow, + windowId, + } = this.props; + + if (windowId) { + updateWindow(windowId, { + canvasId: null, collectionPath: [...collectionPath, manifestId], manifestId: m.id, + }); + } else { + addWindow({ collectionPath: [...collectionPath, manifestId], manifestId: m.id }); + } + + hideCollectionDialog(); + setWorkspaceAddVisibility(false); + } + + /** */ + placeholder() { + const { classes, hideCollectionDialog } = this.props; + + return ( + <Dialog + onClose={hideCollectionDialog} + open + > + <DialogTitle id="select-collection" disableTypography> + <Skeleton className={classes.placeholder} variant="text" /> + </DialogTitle> + <ScrollIndicatedDialogContent> + <Skeleton className={classes.placeholder} variant="text" /> + <Skeleton className={classes.placeholder} variant="text" /> + </ScrollIndicatedDialogContent> + </Dialog> + ); + } + + /** */ + render() { + const { + classes, + collection, + error, + hideCollectionDialog, + isMultipart, + manifest, + ready, + t, + } = this.props; + + const { filter } = this.state; + + if (error) return null; + if (!ready) return this.placeholder(); + + const rights = manifest && (asArray(manifest.getProperty('rights') || manifest.getProperty('license'))); + + const requiredStatement = manifest + && asArray(manifest.getRequiredStatement()).filter(l => l.getValue()).map(labelValuePair => ({ + label: labelValuePair.getLabel(), + values: labelValuePair.getValues(), + })); + + const collections = manifest.getCollections(); + + const currentFilter = filter || (collections.length > 0 ? 'collections' : 'manifests'); + + return ( + <Dialog + onClose={hideCollectionDialog} + open + > + <DialogTitle id="select-collection" disableTypography> + <Typography component="div" variant="overline"> + { t(isMultipart ? 'multipartCollection' : 'collection') } + </Typography> + <Typography variant="h3"> + {CollectionDialog.getUseableLabel(manifest)} + </Typography> + </DialogTitle> + <ScrollIndicatedDialogContent className={classes.dialogContent}> + { collection && ( + <Button + startIcon={<ArrowBackIcon />} + onClick={() => this.goToPreviousCollection()} + > + {CollectionDialog.getUseableLabel(collection)} + </Button> + )} + + <div className={classes.collectionMetadata}> + <ManifestInfo manifestId={manifest.id} /> + <CollapsibleSection + id="select-collection-rights" + label={t('attributionTitle')} + > + { requiredStatement && ( + <LabelValueMetadata labelValuePairs={requiredStatement} defaultLabel={t('attribution')} /> + )} + { + rights && rights.length > 0 && ( + <> + <Typography variant="subtitle2" component="dt">{t('rights')}</Typography> + { rights.map(v => ( + <Typography variant="body1" component="dd"> + <Link target="_blank" rel="noopener noreferrer" href={v}> + {v} + </Link> + </Typography> + )) } + </> + ) + } + </CollapsibleSection> + </div> + <div className={classes.collectionFilter}> + {manifest.getTotalCollections() > 0 && ( + <Chip clickable color={currentFilter === 'collections' ? 'primary' : 'default'} onClick={() => this.setFilter('collections')} label={t('totalCollections', { count: manifest.getTotalCollections() })} /> + )} + {manifest.getTotalManifests() > 0 && ( + <Chip clickable color={currentFilter === 'manifests' ? 'primary' : 'default'} onClick={() => this.setFilter('manifests')} label={t('totalManifests', { count: manifest.getTotalManifests() })} /> + )} + </div> + { currentFilter === 'collections' && ( + <MenuList> + { + collections.map(c => ( + <MenuItem key={c.id} onClick={() => { this.selectCollection(c); }}> + {CollectionDialog.getUseableLabel(c)} + </MenuItem> + )) + } + </MenuList> + )} + { currentFilter === 'manifests' && ( + <MenuList> + { + manifest.getManifests().map(m => ( + <MenuItem key={m.id} onClick={() => { this.selectManifest(m); }}> + {CollectionDialog.getUseableLabel(m)} + </MenuItem> + )) + } + </MenuList> + )} + </ScrollIndicatedDialogContent> + <DialogActions> + <Button onClick={hideCollectionDialog}> + {t('close')} + </Button> + </DialogActions> + </Dialog> + ); + } +} + +CollectionDialog.propTypes = { + addWindow: PropTypes.func.isRequired, + classes: PropTypes.objectOf(PropTypes.string).isRequired, + collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types + collectionPath: PropTypes.arrayOf(PropTypes.string), + error: PropTypes.string, + hideCollectionDialog: PropTypes.func.isRequired, + isMultipart: PropTypes.bool, + manifest: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + manifestId: PropTypes.string.isRequired, + ready: PropTypes.bool, + setWorkspaceAddVisibility: PropTypes.func.isRequired, + showCollectionDialog: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, + updateWindow: PropTypes.func.isRequired, + windowId: PropTypes.string, +}; + +CollectionDialog.defaultProps = { + collection: null, + collectionPath: [], + error: null, + isMultipart: false, + ready: false, + windowId: null, +}; diff --git a/src/components/CollectionInfo.js b/src/components/CollectionInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..15670b1c98db518dddeac26431b457bf1e1596a5 --- /dev/null +++ b/src/components/CollectionInfo.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import ViewListIcon from '@material-ui/icons/ViewListSharp'; +import CollapsibleSection from '../containers/CollapsibleSection'; + +/** + * ManifestInfo + */ +export class CollectionInfo extends Component { + /** */ + constructor(props) { + super(props); + + this.openCollectionDialog = this.openCollectionDialog.bind(this); + } + + /** */ + openCollectionDialog() { + const { collectionPath, showCollectionDialog, windowId } = this.props; + + const manifestId = collectionPath[collectionPath.length - 1]; + + showCollectionDialog(manifestId, collectionPath.slice(0, -1), windowId); + } + + /** + * render + * @return + */ + render() { + const { + collectionLabel, + collectionPath, + id, + t, + } = this.props; + + if (collectionPath.length === 0) return null; + + return ( + <CollapsibleSection + id={`${id}-collection`} + label={t('collection')} + > + {collectionLabel && ( + <Typography + aria-labelledby={`${id}-resource ${id}-resource-heading`} + id={`${id}-resource-heading`} + variant="h4" + > + {collectionLabel} + </Typography> + )} + + <Button + color="primary" + onClick={this.openCollectionDialog} + startIcon={<ViewListIcon />} + > + {t('showCollection')} + </Button> + </CollapsibleSection> + ); + } +} + +CollectionInfo.propTypes = { + collectionLabel: PropTypes.string, + collectionPath: PropTypes.arrayOf(PropTypes.string), + id: PropTypes.string.isRequired, + showCollectionDialog: PropTypes.func.isRequired, + t: PropTypes.func, + windowId: PropTypes.string, +}; + +CollectionInfo.defaultProps = { + collectionLabel: null, + collectionPath: [], + t: key => key, + windowId: null, +}; diff --git a/src/components/ManifestListItem.js b/src/components/ManifestListItem.js index bf70127676f4640847963f53dab8521d98f41402..8333ca28714ae51568bf20f6b3b1742a9ba959e3 100644 --- a/src/components/ManifestListItem.js +++ b/src/components/ManifestListItem.js @@ -9,12 +9,6 @@ import { Img } from 'react-image'; import ManifestListItemError from '../containers/ManifestListItemError'; import ns from '../config/css-ns'; -/** - * Handling open button click - */ -const handleOpenButtonClick = (event, manifest, addWindow) => { - addWindow({ manifestId: manifest }); -}; /** * Represents an item in a list of currently-loaded or loading manifests * @param {object} props @@ -23,6 +17,12 @@ const handleOpenButtonClick = (event, manifest, addWindow) => { /** */ export class ManifestListItem extends React.Component { + /** */ + constructor(props) { + super(props); + this.handleOpenButtonClick = this.handleOpenButtonClick.bind(this); + } + /** */ componentDidMount() { const { @@ -32,6 +32,26 @@ export class ManifestListItem extends React.Component { if (!ready && !error && !isFetching && provider !== 'file') fetchManifest(manifestId); } + /** + * Handling open button click + */ + handleOpenButtonClick() { + const { + addWindow, + handleClose, + manifestId, + showCollectionDialog, + isCollection, + } = this.props; + + if (isCollection) { + showCollectionDialog(manifestId); + } else { + addWindow({ manifestId }); + handleClose(); + } + } + /** */ render() { const { @@ -42,13 +62,13 @@ export class ManifestListItem extends React.Component { title, thumbnail, manifestLogo, - addWindow, - handleClose, size, classes, provider, t, error, + isCollection, + isMultipart, } = this.props; const placeholder = ( @@ -86,9 +106,7 @@ export class ManifestListItem extends React.Component { ref={buttonRef} className={ns('manifest-list-item-title')} style={{ width: '100%' }} - onClick={ - (event) => { handleOpenButtonClick(event, manifestId, addWindow); handleClose(); } - } + onClick={this.handleOpenButtonClick} > <Grid container spacing={2} className={classes.label} component="span"> <Grid item xs={4} sm={3} component="span"> @@ -109,6 +127,11 @@ export class ManifestListItem extends React.Component { /> </Grid> <Grid item xs={8} sm={9} component="span"> + { isCollection && ( + <Typography component="div" variant="overline"> + { t(isMultipart ? 'multipartCollection' : 'collection') } + </Typography> + )} <Typography component="span" variant="h6"> {title || manifestId} </Typography> @@ -155,11 +178,14 @@ ManifestListItem.propTypes = { error: PropTypes.string, fetchManifest: PropTypes.func.isRequired, handleClose: PropTypes.func, + isCollection: PropTypes.bool, isFetching: PropTypes.bool, + isMultipart: PropTypes.bool, manifestId: PropTypes.string.isRequired, manifestLogo: PropTypes.string, provider: PropTypes.string, ready: PropTypes.bool, + showCollectionDialog: PropTypes.func.isRequired, size: PropTypes.number, t: PropTypes.func, thumbnail: PropTypes.string, @@ -172,7 +198,9 @@ ManifestListItem.defaultProps = { classes: {}, error: null, handleClose: () => {}, + isCollection: false, isFetching: false, + isMultipart: false, manifestLogo: null, provider: null, ready: false, diff --git a/src/components/WindowSideBarCanvasPanel.js b/src/components/WindowSideBarCanvasPanel.js index 76ca76d8f918651b083d2262bc68e1dfa93ac93d..0100d965d0329b39620dc8dc2e16901cfe1eb717 100644 --- a/src/components/WindowSideBarCanvasPanel.js +++ b/src/components/WindowSideBarCanvasPanel.js @@ -3,10 +3,13 @@ import PropTypes from 'prop-types'; import Tabs from '@material-ui/core/Tabs'; import Tab from '@material-ui/core/Tab'; import Tooltip from '@material-ui/core/Tooltip'; +import Button from '@material-ui/core/Button'; import RootRef from '@material-ui/core/RootRef'; import ItemListIcon from '@material-ui/icons/ReorderSharp'; 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 CompanionWindow from '../containers/CompanionWindow'; import SidebarIndexList from '../containers/SidebarIndexList'; import SidebarIndexTableOfContents from '../containers/SidebarIndexTableOfContents'; @@ -23,6 +26,15 @@ export class WindowSideBarCanvasPanel extends Component { this.containerRef = React.createRef(); } + /** */ + static getUseableLabel(resource, index) { + return (resource + && resource.getLabel + && resource.getLabel().length > 0) + ? resource.getLabel().map(label => label.value)[0] + : resource.id; + } + /** @private */ handleVariantChange(event, value) { const { updateVariant } = this.props; @@ -36,7 +48,9 @@ export class WindowSideBarCanvasPanel extends Component { render() { const { classes, + collection, id, + showMultipart, t, variant, showToc, @@ -44,6 +58,7 @@ export class WindowSideBarCanvasPanel extends Component { } = this.props; let listComponent; + if (variant === 'tableOfContents') { listComponent = ( <SidebarIndexTableOfContents @@ -85,6 +100,17 @@ export class WindowSideBarCanvasPanel extends Component { )} > <div id={`tab-panel-${id}`}> + { collection && ( + <Button + fullWidth + onClick={showMultipart} + endIcon={<ArrowForwardIcon />} + > + <Typography className={classes.collectionNavigationButton}> + {WindowSideBarCanvasPanel.getUseableLabel(collection)} + </Typography> + </Button> + )} {listComponent} </div> </CompanionWindow> @@ -95,7 +121,9 @@ export class WindowSideBarCanvasPanel extends Component { WindowSideBarCanvasPanel.propTypes = { classes: PropTypes.objectOf(PropTypes.string).isRequired, + collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types id: PropTypes.string.isRequired, + showMultipart: PropTypes.func.isRequired, showToc: PropTypes.bool, t: PropTypes.func.isRequired, updateVariant: PropTypes.func.isRequired, @@ -104,5 +132,6 @@ WindowSideBarCanvasPanel.propTypes = { }; WindowSideBarCanvasPanel.defaultProps = { + collection: null, showToc: false, }; diff --git a/src/components/WindowSideBarCollectionPanel.js b/src/components/WindowSideBarCollectionPanel.js new file mode 100644 index 0000000000000000000000000000000000000000..baa41c951c26e1fea9571e3a0afe6e3b04b34ca6 --- /dev/null +++ b/src/components/WindowSideBarCollectionPanel.js @@ -0,0 +1,186 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import MenuList from '@material-ui/core/MenuList'; +import MenuItem from '@material-ui/core/MenuItem'; +import Typography from '@material-ui/core/Typography'; +import Skeleton from '@material-ui/lab/Skeleton'; +import ArrowUpwardIcon from '@material-ui/icons/ArrowUpwardSharp'; +import CompanionWindow from '../containers/CompanionWindow'; +import IIIFThumbnail from '../containers/IIIFThumbnail'; + +/** */ +export class WindowSideBarCollectionPanel extends Component { + /** */ + static getUseableLabel(resource, index) { + return (resource + && resource.getLabel + && resource.getLabel().length > 0) + ? resource.getLabel().map(label => label.value)[0] + : resource.id; + } + + /** */ + isMultipart() { + const { collection } = this.props; + + if (!collection) return false; + + const behaviors = collection.getProperty('behavior'); + + if (Array.isArray(behaviors)) return collection.includes('multi-part'); + + return behaviors === 'multi-part'; + } + + /** */ + render() { + const { + canvasNavigation, + classes, + collectionPath, + collection, + id, + isFetching, + manifestId, + parentCollection, + updateCompanionWindow, + updateWindow, + t, + variant, + windowId, + } = this.props; + + /** */ + const Item = ({ manifest, ...otherProps }) => ( + <MenuItem + className={classes.menuItem} + alignItems="flex-start" + button + component="li" + selected={manifestId === manifest.id} + {...otherProps} + > + { variant === 'thumbnail' && ( + <ListItemIcon> + <IIIFThumbnail + resource={manifest} + maxHeight={canvasNavigation.height} + maxWidth={canvasNavigation.width} + /> + </ListItemIcon> + )} + <ListItemText>{WindowSideBarCollectionPanel.getUseableLabel(manifest)}</ListItemText> + </MenuItem> + ); + + return ( + <CompanionWindow + title={t(this.isMultipart() ? 'multipartCollection' : 'collection')} + windowId={windowId} + id={id} + titleControls={( + <> + { parentCollection && ( + <List> + <ListItem + button + onClick={ + () => updateCompanionWindow({ collectionPath: collectionPath.slice(0, -1) }) + } + > + <ListItemIcon> + <ArrowUpwardIcon /> + </ListItemIcon> + <ListItemText primaryTypographyProps={{ variant: 'body1' }}> + {WindowSideBarCollectionPanel.getUseableLabel(parentCollection)} + </ListItemText> + </ListItem> + </List> + )} + <Typography variant="h6"> + { collection && WindowSideBarCollectionPanel.getUseableLabel(collection)} + { isFetching && <Skeleton className={classes.placeholder} variant="text" />} + </Typography> + </> + )} + > + <MenuList> + { isFetching && ( + <MenuItem> + <ListItemText> + <Skeleton className={classes.placeholder} variant="text" /> + <Skeleton className={classes.placeholder} variant="text" /> + <Skeleton className={classes.placeholder} variant="text" /> + </ListItemText> + </MenuItem> + )} + { + collection && collection.getCollections().map((manifest) => { + /** select the new manifest and go back to the normal index */ + const onClick = () => { + // close collection + updateCompanionWindow({ collectionPath: [...collectionPath, manifest.id] }); + }; + + return ( + <Item key={manifest.id} onClick={onClick} manifest={manifest} /> + ); + }) + } + { + collection && collection.getManifests().map((manifest) => { + /** select the new manifest and go back to the normal index */ + const onClick = () => { + // select new manifest + updateWindow({ canvasId: null, collectionPath, manifestId: manifest.id }); + // close collection + updateCompanionWindow({ multipart: false }); + }; + + return ( + <Item key={manifest.id} onClick={onClick} manifest={manifest} /> + ); + }) + } + </MenuList> + </CompanionWindow> + ); + } +} + +WindowSideBarCollectionPanel.propTypes = { + canvasNavigation: PropTypes.shape({ + height: PropTypes.number, + width: PropTypes.number, + }).isRequired, + classes: PropTypes.objectOf(PropTypes.string).isRequired, + collection: PropTypes.object, // eslint-disable-line react/forbid-prop-types + collectionId: PropTypes.string.isRequired, + collectionPath: PropTypes.arrayOf(PropTypes.string), + error: PropTypes.string, + id: PropTypes.string.isRequired, + isFetching: PropTypes.bool, + manifestId: PropTypes.string.isRequired, + parentCollection: PropTypes.object, // eslint-disable-line react/forbid-prop-types + ready: PropTypes.bool, + t: PropTypes.func, + updateCompanionWindow: PropTypes.func.isRequired, + updateWindow: PropTypes.func.isRequired, + variant: PropTypes.string, + windowId: PropTypes.string.isRequired, +}; + +WindowSideBarCollectionPanel.defaultProps = { + collection: null, + collectionPath: [], + error: null, + isFetching: false, + parentCollection: null, + ready: false, + t: k => k, + variant: null, +}; diff --git a/src/components/WindowSideBarInfoPanel.js b/src/components/WindowSideBarInfoPanel.js index f2530d26152e7fa47dc6a518d971a1f706bbabe3..176e0761c564c214b4f95cbd43a309a5d380490a 100644 --- a/src/components/WindowSideBarInfoPanel.js +++ b/src/components/WindowSideBarInfoPanel.js @@ -4,6 +4,7 @@ import CompanionWindow from '../containers/CompanionWindow'; import CanvasInfo from '../containers/CanvasInfo'; import LocalePicker from '../containers/LocalePicker'; import ManifestInfo from '../containers/ManifestInfo'; +import CollectionInfo from '../containers/CollectionInfo'; import ManifestRelatedLinks from '../containers/ManifestRelatedLinks'; import ns from '../config/css-ns'; @@ -20,6 +21,7 @@ export class WindowSideBarInfoPanel extends Component { windowId, id, classes, + collectionPath, t, locale, selectedCanvases, @@ -58,6 +60,11 @@ export class WindowSideBarInfoPanel extends Component { </div> )) } + { collectionPath.length > 0 && ( + <div className={classes.section}> + <CollectionInfo id={id} windowId={windowId} /> + </div> + )} <div className={classes.section}> <ManifestInfo id={id} windowId={windowId} /> @@ -74,6 +81,7 @@ export class WindowSideBarInfoPanel extends Component { WindowSideBarInfoPanel.propTypes = { availableLocales: PropTypes.arrayOf(PropTypes.string), classes: PropTypes.objectOf(PropTypes.string), + collectionPath: PropTypes.arrayOf(PropTypes.string), id: PropTypes.string.isRequired, locale: PropTypes.string, selectedCanvases: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string })), @@ -86,6 +94,7 @@ WindowSideBarInfoPanel.propTypes = { WindowSideBarInfoPanel.defaultProps = { availableLocales: [], classes: {}, + collectionPath: [], locale: '', selectedCanvases: [], setLocale: undefined, diff --git a/src/components/WorkspaceArea.js b/src/components/WorkspaceArea.js index 83439c3a7875e03239de7323a1cf987d5765694e..672c6a762dcaa64b4a10b96af4af5a1eabd9ff0e 100644 --- a/src/components/WorkspaceArea.js +++ b/src/components/WorkspaceArea.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import CollectionDialog from '../containers/CollectionDialog'; import ErrorDialog from '../containers/ErrorDialog'; import WorkspaceControlPanel from '../containers/WorkspaceControlPanel'; import Workspace from '../containers/Workspace'; @@ -19,8 +20,13 @@ export class WorkspaceArea extends Component { */ render() { const { - classes, controlPanelVariant, isWorkspaceAddVisible, isWorkspaceControlPanelVisible, t, + classes, + controlPanelVariant, + isCollectionDialogVisible, + isWorkspaceAddVisible, + isWorkspaceControlPanelVisible, lang, + t, } = this.props; return ( @@ -41,6 +47,7 @@ export class WorkspaceArea extends Component { } <ErrorDialog /> <BackgroundPluginArea /> + { isCollectionDialogVisible && <CollectionDialog /> } </main> </> ); @@ -50,6 +57,7 @@ export class WorkspaceArea extends Component { WorkspaceArea.propTypes = { classes: PropTypes.objectOf(PropTypes.string).isRequired, controlPanelVariant: PropTypes.string, + isCollectionDialogVisible: PropTypes.bool, isWorkspaceAddVisible: PropTypes.bool, isWorkspaceControlPanelVisible: PropTypes.bool.isRequired, lang: PropTypes.string, @@ -58,6 +66,7 @@ WorkspaceArea.propTypes = { WorkspaceArea.defaultProps = { controlPanelVariant: undefined, + isCollectionDialogVisible: false, isWorkspaceAddVisible: false, lang: undefined, }; diff --git a/src/components/WorkspaceMosaic.js b/src/components/WorkspaceMosaic.js index 749aef84bbeadd947c68dd32effc5e051ff2bcf9..f065ac60041750edc1ecb44f07aa47b5f0439006 100644 --- a/src/components/WorkspaceMosaic.js +++ b/src/components/WorkspaceMosaic.js @@ -5,7 +5,6 @@ import { } from 'react-mosaic-component'; import 'react-mosaic-component/react-mosaic-component.css'; import difference from 'lodash/difference'; -import toPairs from 'lodash/toPairs'; import isEqual from 'lodash/isEqual'; import classNames from 'classnames'; import MosaicRenderPreview from '../containers/MosaicRenderPreview'; diff --git a/src/containers/CollectionDialog.js b/src/containers/CollectionDialog.js new file mode 100644 index 0000000000000000000000000000000000000000..8c1f687ed52e57b50e65a8223a5da446a47d3ebb --- /dev/null +++ b/src/containers/CollectionDialog.js @@ -0,0 +1,84 @@ +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/core'; +import { withTranslation } from 'react-i18next'; +import { withPlugins } from '../extend/withPlugins'; +import * as actions from '../state/actions'; +import { getManifest, getManifestoInstance, getSequenceBehaviors } from '../state/selectors'; +import { CollectionDialog } from '../components/CollectionDialog'; + +/** + * mapDispatchToProps - used to hook up connect to action creators + * @memberof CollectionDialog + * @private + */ +const mapDispatchToProps = { + addWindow: actions.addWindow, + hideCollectionDialog: actions.hideCollectionDialog, + setWorkspaceAddVisibility: actions.setWorkspaceAddVisibility, + showCollectionDialog: actions.showCollectionDialog, + updateWindow: actions.updateWindow, +}; + +/** + * mapStateToProps - to hook up connect + * @memberof CollectionDialog + * @private + */ +const mapStateToProps = (state) => { + const { collectionPath, collectionManifestId: manifestId } = state.workspace; + const manifest = getManifest(state, { manifestId }); + + const collectionId = collectionPath && collectionPath[collectionPath.length - 1]; + const collection = collectionId && getManifest(state, { manifestId: collectionId }); + + return { + collection: collection && getManifestoInstance(state, { manifestId: collection.id }), + collectionPath, + error: manifest && manifest.error, + isMultipart: getSequenceBehaviors(state, { manifestId }).includes('multi-part'), + manifest: manifest && getManifestoInstance(state, { manifestId }), + manifestId, + open: state.workspace.collectionDialogOn, + ready: manifest && !!manifest.json, + windowId: state.workspace.collectionUpdateWindowId, + }; +}; + +/** */ +const styles = theme => ({ + collectionFilter: { + padding: '16px', + paddingTop: 0, + }, + collectionMetadata: { + padding: '16px', + }, + dark: { + color: '#000000', + }, + dialogContent: { + padding: 0, + }, + light: { + color: theme.palette.grey[400], + }, + listitem: { + '&:focus': { + backgroundColor: theme.palette.action.focus, + }, + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, + cursor: 'pointer', + }, +}); + +const enhance = compose( + withTranslation(), + withStyles(styles), + connect(mapStateToProps, mapDispatchToProps), + withPlugins('CollectionDialog'), +); + +export default enhance(CollectionDialog); diff --git a/src/containers/CollectionInfo.js b/src/containers/CollectionInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..ca686c0a5922f906c6d2ff8f66fa95891b09f424 --- /dev/null +++ b/src/containers/CollectionInfo.js @@ -0,0 +1,37 @@ +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; +import { withPlugins } from '../extend/withPlugins'; +import { + getManifestTitle, + getWindow, +} from '../state/selectors'; +import * as actions from '../state/actions'; +import { CollectionInfo } from '../components/CollectionInfo'; + +/** + * mapStateToProps - to hook up connect + * @memberof WindowSideBarInfoPanel + * @private + */ +const mapStateToProps = (state, { id, windowId }) => { + const { collectionPath } = (getWindow(state, { windowId }) || {}); + const manifestId = collectionPath[collectionPath.length - 1]; + + return { + collectionLabel: getManifestTitle(state, { manifestId }), + collectionPath, + }; +}; + +const mapDispatchToProps = { + showCollectionDialog: actions.showCollectionDialog, +}; + +const enhance = compose( + withTranslation(), + connect(mapStateToProps, mapDispatchToProps), + withPlugins('CollectionInfo'), +); + +export default enhance(CollectionInfo); diff --git a/src/containers/ManifestInfo.js b/src/containers/ManifestInfo.js index 5ae7a95a3f5f3a1df8729bc510730775db226bd1..737b87cf7ab78b17ca91e41e1d5b61e31a8876cb 100644 --- a/src/containers/ManifestInfo.js +++ b/src/containers/ManifestInfo.js @@ -14,10 +14,12 @@ import { ManifestInfo } from '../components/ManifestInfo'; * @memberof WindowSideBarInfoPanel * @private */ -const mapStateToProps = (state, { id, windowId }) => ({ - manifestDescription: getManifestDescription(state, { companionWindowId: id, windowId }), - manifestLabel: getManifestTitle(state, { companionWindowId: id, windowId }), - manifestMetadata: getManifestMetadata(state, { companionWindowId: id, windowId }), +const mapStateToProps = (state, { id, manifestId, windowId }) => ({ + manifestDescription: getManifestDescription(state, { + companionWindowId: id, manifestId, windowId, + }), + manifestLabel: getManifestTitle(state, { companionWindowId: id, manifestId, windowId }), + manifestMetadata: getManifestMetadata(state, { companionWindowId: id, manifestId, windowId }), }); const enhance = compose( diff --git a/src/containers/ManifestListItem.js b/src/containers/ManifestListItem.js index 596c8a69e46097d7c2d1edb40d07c0fa1d08c098..20f2db58ae2132e4a32386d35d1b4a7dfc610318 100644 --- a/src/containers/ManifestListItem.js +++ b/src/containers/ManifestListItem.js @@ -7,6 +7,7 @@ import { getManifest, getManifestTitle, getManifestThumbnail, getCanvases, getManifestLogo, getManifestProvider, getWindowManifests, + getManifestoInstance, getSequenceBehaviors, } from '../state/selectors'; import * as actions from '../state/actions'; import { ManifestListItem } from '../components/ManifestListItem'; @@ -14,15 +15,26 @@ import { ManifestListItem } from '../components/ManifestListItem'; /** */ const mapStateToProps = (state, { manifestId, provider }) => { const manifest = getManifest(state, { manifestId }) || {}; + const manifesto = getManifestoInstance(state, { manifestId }); + const isCollection = ( + manifesto || { isCollection: () => false } + ).isCollection(); + + const size = isCollection + ? manifesto.getTotalItems() + : getCanvases(state, { manifestId }).length; return { active: getWindowManifests(state).includes(manifestId), error: manifest.error, + isCollection, isFetching: manifest.isFetching, + isMultipart: isCollection + && getSequenceBehaviors(state, { manifestId }).includes('multi-part'), manifestLogo: getManifestLogo(state, { manifestId }), provider: provider || getManifestProvider(state, { manifestId }), ready: !!manifest.json, - size: getCanvases(state, { manifestId }).length, + size, thumbnail: getManifestThumbnail(state, { manifestId }), title: getManifestTitle(state, { manifestId }), }; @@ -33,7 +45,11 @@ const mapStateToProps = (state, { manifestId, provider }) => { * @memberof ManifestListItem * @private */ -const mapDispatchToProps = { addWindow: actions.addWindow, fetchManifest: actions.fetchManifest }; +const mapDispatchToProps = { + addWindow: actions.addWindow, + fetchManifest: actions.fetchManifest, + showCollectionDialog: actions.showCollectionDialog, +}; /** * diff --git a/src/containers/WindowSideBarCanvasPanel.js b/src/containers/WindowSideBarCanvasPanel.js index d2beb97166323a85c59bb36f3dcfa23cf51492da..a55744b1cb0b8b51008831ce1b70ede2629642e3 100644 --- a/src/containers/WindowSideBarCanvasPanel.js +++ b/src/containers/WindowSideBarCanvasPanel.js @@ -11,6 +11,8 @@ import { getCanvases, getVisibleCanvases, getSequenceTreeStructure, + getWindow, + getManifestoInstance, } from '../state/selectors'; /** @@ -19,13 +21,18 @@ import { const mapStateToProps = (state, { id, windowId }) => { const canvases = getCanvases(state, { windowId }); const treeStructure = getSequenceTreeStructure(state, { windowId }); + const window = getWindow(state, { windowId }); const { config } = state; + const companionWindow = getCompanionWindow(state, { companionWindowId: id }); + const collectionPath = window.collectionPath || []; + const collectionId = collectionPath && collectionPath[collectionPath.length - 1]; return { canvases, + collection: collectionId && getManifestoInstance(state, { manifestId: collectionId }), config, selectedCanvases: getVisibleCanvases(state, { windowId }), showToc: treeStructure && treeStructure.nodes && treeStructure.nodes.length > 0, - variant: getCompanionWindow(state, { companionWindowId: id, windowId }).variant + variant: companionWindow.variant || getDefaultSidebarVariant(state, { windowId }), }; }; @@ -37,6 +44,9 @@ const mapStateToProps = (state, { id, windowId }) => { */ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ setCanvas: (...args) => dispatch(actions.setCanvas(...args)), + showMultipart: () => dispatch( + actions.addOrUpdateCompanionWindow(windowId, { content: 'collection', position: 'right' }), + ), toggleDraggingEnabled: () => dispatch(actions.toggleDraggingEnabled()), updateVariant: variant => dispatch( actions.updateCompanionWindow(windowId, id, { variant }), @@ -48,6 +58,9 @@ const mapDispatchToProps = (dispatch, { id, windowId }) => ({ * @param theme */ const styles = theme => ({ + collectionNavigationButton: { + textTransform: 'none', + }, label: { paddingLeft: theme.spacing(1), }, diff --git a/src/containers/WindowSideBarCollectionPanel.js b/src/containers/WindowSideBarCollectionPanel.js new file mode 100644 index 0000000000000000000000000000000000000000..32b31a77b283f90756a568cffe44be7282f31d5b --- /dev/null +++ b/src/containers/WindowSideBarCollectionPanel.js @@ -0,0 +1,80 @@ +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; +import { withStyles } from '@material-ui/core/styles'; +import { withPlugins } from '../extend/withPlugins'; +import * as actions from '../state/actions'; +import { + getCompanionWindow, + getManifest, + getManifestoInstance, + getDefaultSidebarVariant, + getWindow, +} from '../state/selectors'; +import { WindowSideBarCollectionPanel } from '../components/WindowSideBarCollectionPanel'; + +/** + * mapStateToProps - to hook up connect + */ +const mapStateToProps = (state, { id, windowId }) => { + const window = getWindow(state, { windowId }); + const companionWindow = getCompanionWindow(state, { companionWindowId: id }); + const { collectionPath: localCollectionPath } = companionWindow; + const collectionPath = localCollectionPath || window.collectionPath; + const collectionId = collectionPath && collectionPath[collectionPath.length - 1]; + const parentCollectionId = collectionPath && collectionPath[collectionPath.length - 2]; + const collection = collectionId && getManifest(state, { manifestId: collectionId }); + const parentCollection = parentCollectionId + && getManifest(state, { manifestId: parentCollectionId }); + const manifest = getManifest(state, { windowId }); + + return { + canvasNavigation: state.config.canvasNavigation, + collection: collection && getManifestoInstance(state, { manifestId: collection.id }), + collectionId, + collectionPath, + error: collection && collection.error, + isFetching: collection && collection.isFetching, + manifestId: manifest && manifest.id, + parentCollection: parentCollection + && getManifestoInstance(state, { manifestId: parentCollection.id }), + ready: collection && !!collection.json, + variant: companionWindow.variant + || getDefaultSidebarVariant(state, { windowId }), + }; +}; + +/** + * mapStateToProps - used to hook up connect to state + * @memberof SidebarIndexList + * @private + */ +const mapDispatchToProps = (dispatch, { id, windowId }) => ({ + updateCompanionWindow: (...args) => dispatch( + actions.updateCompanionWindow(windowId, id, ...args), + ), + updateWindow: (...args) => dispatch(actions.updateWindow(windowId, ...args)), +}); + +/** + * Styles for withStyles HOC + */ +const styles = theme => ({ + label: { + paddingLeft: theme.spacing(1), + }, + menuItem: { + borderBottom: `0.5px solid ${theme.palette.divider}`, + paddingRight: theme.spacing(1), + whiteSpace: 'normal', + }, +}); + +const enhance = compose( + withStyles(styles), + withTranslation(), + connect(mapStateToProps, mapDispatchToProps), + withPlugins('WindowSideBarCollectionPanel'), +); + +export default enhance(WindowSideBarCollectionPanel); diff --git a/src/containers/WindowSideBarInfoPanel.js b/src/containers/WindowSideBarInfoPanel.js index 812dfad47ee483978bb62c1ef08746fda2f01250..0a69e62d0ebe70f5cc9079aea63e48c2725b1c94 100644 --- a/src/containers/WindowSideBarInfoPanel.js +++ b/src/containers/WindowSideBarInfoPanel.js @@ -10,6 +10,7 @@ import { getMetadataLocales, getVisibleCanvases, getWindowConfig, + getWindow, } from '../state/selectors'; import { WindowSideBarInfoPanel } from '../components/WindowSideBarInfoPanel'; @@ -20,6 +21,7 @@ import { WindowSideBarInfoPanel } from '../components/WindowSideBarInfoPanel'; */ const mapStateToProps = (state, { id, windowId }) => ({ availableLocales: getMetadataLocales(state, { companionWindowId: id, windowId }), + collectionPath: (getWindow(state, { windowId }) || {}).collectionPath, locale: getCompanionWindow(state, { companionWindowId: id }).locale || getManifestLocale(state, { windowId }), selectedCanvases: getVisibleCanvases(state, { windowId }), diff --git a/src/containers/WorkspaceArea.js b/src/containers/WorkspaceArea.js index c2655978c98e9a934c485eccbf5984192b2aa31a..a9f7bc2a35c800ab2216976eb342558745ff017f 100644 --- a/src/containers/WorkspaceArea.js +++ b/src/containers/WorkspaceArea.js @@ -14,6 +14,7 @@ import { getConfig, getWindowIds, getWorkspace } from '../state/selectors'; const mapStateToProps = state => ( { controlPanelVariant: getWorkspace(state).isWorkspaceAddVisible || getWindowIds(state).length > 0 ? undefined : 'wide', + isCollectionDialogVisible: getWorkspace(state).collectionDialogOn, isWorkspaceAddVisible: getWorkspace(state).isWorkspaceAddVisible, isWorkspaceControlPanelVisible: getConfig(state).workspaceControlPanel.enabled, lang: getConfig(state).language, diff --git a/src/lib/CompanionWindowRegistry.js b/src/lib/CompanionWindowRegistry.js index 5197a0af1c54aa99279320e960ccd7e5abeb1381..6ba0b1ea308fec63d3be35d9a17f6067f5cb8c4d 100644 --- a/src/lib/CompanionWindowRegistry.js +++ b/src/lib/CompanionWindowRegistry.js @@ -6,11 +6,13 @@ import AttributionPanel from '../containers/AttributionPanel'; import SearchPanel from '../containers/SearchPanel'; import LayersPanel from '../containers/LayersPanel'; import CustomPanel from '../containers/CustomPanel'; +import WindowSideBarCollectionPanel from '../containers/WindowSideBarCollectionPanel'; const map = { annotations: WindowSideBarAnnotationsPanel, attribution: AttributionPanel, canvas: WindowSideBarCanvasPanel, + collection: WindowSideBarCollectionPanel, custom: CustomPanel, info: WindowSideBarInfoPanel, layers: LayersPanel, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 530decac0945dc38f381bf9549f582c23a6d9503..8c0756f50b71fda54e644df3c939ecf5356dd000 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -118,6 +118,7 @@ "searchTitle": "Search", "selectWorkspaceMenu": "Select workspace type", "showingNumAnnotations": "Showing {{number}} annotations", + "showCollection": "Show collection", "showZoomControls": "Show zoom controls", "sidebarPanelsNavigation": "Sidebar panels navigation", "single": "Single", @@ -129,6 +130,8 @@ "thumbnailNavigation": "Thumbnails", "thumbnails": "Thumbnails", "toggleWindowSideBar": "Toggle sidebar", + "totalCollections": "{{count}} collections", + "totalManifests": "{{count}} manifests", "tryAgain": "Try again", "untitled": "[Untitled]", "view": "View", diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js index 0d88c9781402872b6779be6e02a0dffcdf2d9739..c1f124472712257053c1c755913de1a2e0d09099 100644 --- a/src/state/actions/action-types.js +++ b/src/state/actions/action-types.js @@ -70,6 +70,8 @@ const ActionTypes = { UPDATE_LAYERS: 'mirador/UPDATE_LAYERS', ADD_RESOURCE: 'mirador/ADD_RESOURCE', REMOVE_RESOURCE: 'mirador/REMOVE_RESOURCE', + SHOW_COLLECTION_DIALOG: 'mirador/SHOW_COLLECTION_DIALOG', + HIDE_COLLECTION_DIALOG: 'mirador/HIDE_COLLECTION_DIALOG', }; export default ActionTypes; diff --git a/src/state/actions/workspace.js b/src/state/actions/workspace.js index 0d1d8241b80441b966526e70c0e8ed0eb7f5f4b3..29c52e0f1f36434d121bd983f8be8f2c660b771e 100644 --- a/src/state/actions/workspace.js +++ b/src/state/actions/workspace.js @@ -92,3 +92,20 @@ export function toggleDraggingEnabled() { type: ActionTypes.TOGGLE_DRAGGING, }; } + +/** */ +export function showCollectionDialog(manifestId, collectionPath = [], windowId = null) { + return { + collectionPath, + manifestId, + type: ActionTypes.SHOW_COLLECTION_DIALOG, + windowId, + }; +} + +/** */ +export function hideCollectionDialog() { + return { + type: ActionTypes.HIDE_COLLECTION_DIALOG, + }; +} diff --git a/src/state/reducers/catalog.js b/src/state/reducers/catalog.js index d8dbc195185132bf897f508f5daa3f20675019f7..839270d0fceaf25925f563f777d8425c6487d6ca 100644 --- a/src/state/reducers/catalog.js +++ b/src/state/reducers/catalog.js @@ -5,7 +5,6 @@ import ActionTypes from '../actions/action-types'; */ export const catalogReducer = (state = [], action) => { switch (action.type) { - case ActionTypes.REQUEST_MANIFEST: // falls through, for now at least. case ActionTypes.ADD_RESOURCE: if (state.some(m => m.manifestId === action.manifestId)) return state; @@ -13,6 +12,21 @@ export const catalogReducer = (state = [], action) => { { manifestId: action.manifestId, ...action.payload }, ...state, ]; + case ActionTypes.ADD_WINDOW: + if (state.some(m => m.manifestId === action.window.manifestId)) return state; + + return [ + { manifestId: action.window.manifestId }, + ...state, + ]; + case ActionTypes.UPDATE_WINDOW: + if (!action.payload.manifestId) return state; + if (state.some(m => m.manifestId === action.payload.manifestId)) return state; + + return [ + { manifestId: action.payload.manifestId }, + ...state, + ]; case ActionTypes.REMOVE_RESOURCE: return state.filter(r => r.manifestId !== action.manifestId); case ActionTypes.IMPORT_CONFIG: diff --git a/src/state/reducers/workspace.js b/src/state/reducers/workspace.js index 30d60e4aee6d6cc6693ff4bccfb0772296bdb46a..df6323c587537e22a5e846a2392d34575e2a3c32 100644 --- a/src/state/reducers/workspace.js +++ b/src/state/reducers/workspace.js @@ -99,6 +99,16 @@ export const workspaceReducer = ( return action.state.workspace || {}; case ActionTypes.TOGGLE_DRAGGING: return { ...state, draggingEnabled: !state.draggingEnabled }; + case ActionTypes.SHOW_COLLECTION_DIALOG: + return { + ...state, + collectionDialogOn: true, + collectionManifestId: action.manifestId, + collectionPath: action.collectionPath, + collectionUpdateWindowId: action.windowId, + }; + case ActionTypes.HIDE_COLLECTION_DIALOG: + return { ...state, collectionDialogOn: false }; default: return state; } diff --git a/src/state/sagas/app.js b/src/state/sagas/app.js index ca8bd44d6027775407d475f75cafa78a3c8a6ad6..83a2c407696335ceb736469b5f1eb6a0fe8af72b 100644 --- a/src/state/sagas/app.js +++ b/src/state/sagas/app.js @@ -2,7 +2,7 @@ import { all, call, put, takeEvery, } from 'redux-saga/effects'; import { v4 as uuid } from 'uuid'; -import { fetchManifest } from './iiif'; +import { fetchManifests } from './iiif'; import { fetchWindowManifest } from './windows'; import { addWindow } from '../actions'; import ActionTypes from '../actions/action-types'; @@ -14,7 +14,7 @@ export function* importState(action) { .map(([_, window]) => call(fetchWindowManifest, { id: window.id, payload: window })), ...Object.entries(action.state.manifests || {}) .filter(([_, manifest]) => !manifest.json) - .map(([_, manifest]) => call(fetchManifest, { manifestId: manifest.id })), + .map(([_, manifest]) => call(fetchManifests, manifest.id)), ]); } @@ -41,10 +41,17 @@ export function* importConfig({ config: { thumbnailNavigation, windows } }) { yield all(thunks.map(thunk => put(thunk))); } +/** */ +export function* fetchCollectionManifests(action) { + const { collectionPath, manifestId } = action; + yield call(fetchManifests, manifestId, ...collectionPath); +} + /** */ export default function* appSaga() { yield all([ takeEvery(ActionTypes.IMPORT_MIRADOR_STATE, importState), takeEvery(ActionTypes.IMPORT_CONFIG, importConfig), + takeEvery(ActionTypes.SHOW_COLLECTION_DIALOG, fetchCollectionManifests), ]); } diff --git a/src/state/sagas/iiif.js b/src/state/sagas/iiif.js index d9d635ba70985b0f97440da373413a036486ca41..ee1f8e20599d286895b4af96b6032b0d6dd4b2f0 100644 --- a/src/state/sagas/iiif.js +++ b/src/state/sagas/iiif.js @@ -210,6 +210,16 @@ export function* refetchInfoResponses({ serviceId }) { yield put({ serviceId, type: ActionTypes.CLEAR_ACCESS_TOKEN_QUEUE }); } +/** */ +export function* fetchManifests(...manifestIds) { + const manifests = yield select(getManifests); + + for (let i = 0; i < manifestIds.length; i += 1) { + const manifestId = manifestIds[i]; + if (!manifests[manifestId]) yield call(fetchManifest, { manifestId }); + } +} + /** */ export default function* iiifSaga() { yield all([ diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js index 6f77d0d44fed7d05ff9cf1d8be918d2584a2644c..26c8b9b1aa1ea548c0ec5db74c69629fd045361d 100644 --- a/src/state/sagas/windows.js +++ b/src/state/sagas/windows.js @@ -16,7 +16,7 @@ import { } from '../actions'; import { getSearchForWindow, getSearchAnnotationsForCompanionWindow, - getCanvasGrouping, getWindow, getManifests, getManifestoInstance, + getCanvasGrouping, getWindow, getManifestoInstance, getCompanionWindowIdsForPosition, getManifestSearchService, getCanvasForAnnotation, getSelectedContentSearchAnnotationIds, @@ -27,23 +27,47 @@ import { getCanvases, selectInfoResponses, } from '../selectors'; -import { fetchManifest } from './iiif'; +import { fetchManifests } from './iiif'; /** */ export function* fetchWindowManifest(action) { - const { manifestId } = action.payload || action.window; + const { collectionPath, manifestId } = action.payload || action.window; if (!manifestId) return; if (action.manifest) { yield put(receiveManifest(manifestId, action.manifest)); } else { - const manifests = yield select(getManifests); - if (!manifests[manifestId]) yield call(fetchManifest, { manifestId }); + yield call(fetchManifests, manifestId, ...(collectionPath || [])); } yield call(setWindowStartingCanvas, action); yield call(setWindowDefaultSearchQuery, action); + if (!collectionPath) { + yield call(setCollectionPath, { manifestId, windowId: action.id || action.window.id }); + } +} + +/** */ +export function* setCollectionPath({ manifestId, windowId }) { + const manifestoInstance = yield select(getManifestoInstance, { manifestId }); + + if (manifestoInstance) { + const partOfs = manifestoInstance.getProperty('partOf'); + const partOf = Array.isArray(partOfs) ? partOfs[0] : partOfs; + + if (partOf && partOf.id) { + yield put(updateWindow(windowId, { collectionPath: [partOf.id] })); + } + } +} + +/** */ +export function* fetchCollectionManifests(action) { + const { collectionPath } = action.payload; + if (!collectionPath) return; + + yield call(fetchManifests, ...collectionPath); } /** @private */ @@ -211,6 +235,7 @@ export default function* windowsSaga() { takeEvery(ActionTypes.UPDATE_WINDOW, fetchWindowManifest), takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas), takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses), + takeEvery(ActionTypes.UPDATE_COMPANION_WINDOW, fetchCollectionManifests), takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases), takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult), takeEvery(ActionTypes.SELECT_ANNOTATION, setCanvasforSelectedAnnotation), diff --git a/src/state/selectors/ranges.js b/src/state/selectors/ranges.js index 6ed958040f5c1cbf131d6f870fe9ffc8f6fcfb01..0a40388874a82176fc62b62320bf212d66510679 100644 --- a/src/state/selectors/ranges.js +++ b/src/state/selectors/ranges.js @@ -61,7 +61,7 @@ const getVisibleLeafAndBranchNodeIds = createSelector( getVisibleCanvasIds, ], (tree, canvasIds) => { - if (canvasIds.length === 0) return []; + if (canvasIds.length === 0 || !tree) return []; return getVisibleNodeIdsInSubTree(tree.nodes, canvasIds); }, );