diff --git a/__tests__/src/actions/annotation.test.js b/__tests__/src/actions/annotation.test.js index 7b0be9bf5d98d4ad5d705b9ce7540cb6223430a1..4039f02d48ef1050c7bbf30430e7b793791f6d0f 100644 --- a/__tests__/src/actions/annotation.test.js +++ b/__tests__/src/actions/annotation.test.js @@ -31,23 +31,6 @@ describe('annotation actions', () => { expect(actions.receiveAnnotation(targetId, annotationId, json)).toEqual(expectedAction); }); }); - describe('fetchAnnotation', () => { - describe('success response', () => { - beforeEach(() => { - fetch.mockResponseOnce(JSON.stringify({ data: '12345' })); // eslint-disable-line no-undef - }); - it('dispatches the REQUEST_ANNOTATION action', () => { - expect(actions.fetchAnnotation( - 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', - 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', - )).toEqual({ - annotationId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', - targetId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', - type: 'mirador/REQUEST_ANNOTATION', - }); - }); - }); - }); it('handles the selectAnnotation action', () => { const windowId = 'wId1'; diff --git a/__tests__/src/components/WindowViewer.test.js b/__tests__/src/components/WindowViewer.test.js index ba266aaa84c9b219a1e41d06ed427a6cef912139..373ca1d41366e07eef2a0b194a9d89f0056bff55 100644 --- a/__tests__/src/components/WindowViewer.test.js +++ b/__tests__/src/components/WindowViewer.test.js @@ -1,26 +1,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; import { WindowViewer } from '../../../src/components/WindowViewer'; import OSDViewer from '../../../src/containers/OpenSeadragonViewer'; import WindowCanvasNavigationControls from '../../../src/containers/WindowCanvasNavigationControls'; -import fixture from '../../fixtures/version-2/019.json'; -import emptyCanvasFixture from '../../fixtures/version-2/emptyCanvas.json'; -import otherContentFixture from '../../fixtures/version-2/299843.json'; - -let currentCanvases = Utils.parseManifest(fixture).getSequences()[0].getCanvases(); /** create wrapper */ function createWrapper(props) { return shallow( <WindowViewer - canvasIndex={0} - canvasLabel="label" - infoResponses={{}} - fetchInfoResponse={() => {}} - fetchAnnotation={() => {}} - currentCanvases={[currentCanvases[1]]} - view="single" windowId="xyz" {...props} />, @@ -37,121 +24,4 @@ describe('WindowViewer', () => { </OSDViewer>, )).toBe(true); }); - describe('currentInfoResponses', () => { - describe('returns only available infoResponses', () => { - it('isFetching is false', () => { - wrapper = createWrapper( - { - currentCanvasId: 1, - infoResponses: { - 'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': { - isFetching: false, - }, - 'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410': { - isFetching: true, - }, - }, - view: 'book', - }, - ); - expect(wrapper.instance().currentInfoResponses().length).toEqual(1); - }); - it('infoResponse is undefined', () => { - wrapper = createWrapper( - { - currentCanvasId: 1, - infoResponses: { - foo: { - isFetching: false, - }, - 'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': { - isFetching: false, - }, - }, - view: 'book', - }, - ); - expect(wrapper.instance().currentInfoResponses().length).toEqual(1); - }); - it('error is not present', () => { - wrapper = createWrapper( - { - currentCanvasId: 1, - infoResponses: { - 'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': { - isFetching: false, - }, - 'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410': { - error: 'yikes!', - isFetching: false, - }, - }, - view: 'book', - }, - ); - expect(wrapper.instance().currentInfoResponses().length).toEqual(1); - }); - }); - }); - - describe('componentDidMount', () => { - it('does not call fetchInfoResponse for a canvas that has no images', () => { - const mockFnCanvas0 = jest.fn(); - const mockFnCanvas2 = jest.fn(); - const canvases = Utils.parseManifest(emptyCanvasFixture).getSequences()[0].getCanvases(); - - currentCanvases = [canvases[0]]; - - wrapper = createWrapper( - { - currentCanvases, - currentCanvasId: 0, - fetchInfoResponse: mockFnCanvas0, - view: 'single', - }, - ); - expect(mockFnCanvas0).toHaveBeenCalledTimes(1); - - currentCanvases = [canvases[2]]; - wrapper = createWrapper( - { - currentCanvases, - currentCanvasId: 2, - fetchInfoResponse: mockFnCanvas2, - view: 'single', - }, - ); - expect(mockFnCanvas2).toHaveBeenCalledTimes(0); - }); - it('calls fetchAnnotation when otherContent is present', () => { - const mockFnAnno = jest.fn(); - const canvases = Utils.parseManifest(otherContentFixture).getSequences()[0].getCanvases(); - currentCanvases = [canvases[0]]; - - wrapper = createWrapper( - { currentCanvases, fetchAnnotation: mockFnAnno }, - ); - expect(mockFnAnno).toHaveBeenCalledTimes(1); - }); - }); - - describe('componentDidUpdate', () => { - it('does not call fetchInfoResponse for a canvas that has no images', () => { - const mockFn = jest.fn(); - const canvases = Utils.parseManifest(emptyCanvasFixture).getSequences()[0].getCanvases(); - currentCanvases = [canvases[2]]; - wrapper = createWrapper( - { - currentCanvases, - currentCanvasId: 2, - fetchInfoResponse: mockFn, - view: 'single', - }, - ); - - wrapper.setProps({ currentCanvases: [canvases[3]], currentCanvasId: 3, view: 'single' }); - - expect(mockFn).toHaveBeenCalledTimes(0); - }); - }); }); diff --git a/__tests__/src/lib/MiradorCanvas.test.js b/__tests__/src/lib/MiradorCanvas.test.js index d503b9a1d2c8dbe2e3ee9a16e71a73a3c2a9a41f..e501add183df05b52033abeca3a6e3017daa4e4a 100644 --- a/__tests__/src/lib/MiradorCanvas.test.js +++ b/__tests__/src/lib/MiradorCanvas.test.js @@ -49,27 +49,7 @@ describe('MiradorCanvas', () => { }); }); }); - describe('processAnnotations', () => { - describe('v2', () => { - it('fetches annotations for each annotationList', () => { - const otherContentInstance = new MiradorCanvas( - Utils.parseManifest(otherContentFixture).getSequences()[0].getCanvases()[0], - ); - const fetchMock = jest.fn(); - otherContentInstance.processAnnotations(fetchMock); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - }); - describe('v3', () => { - it('fetches annotations for external items and receives annotations for items that are embedded', () => { - const receiveMock = jest.fn(); - const fetchMock = jest.fn(); - v3Instance.processAnnotations(fetchMock, receiveMock); - expect(receiveMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); - }); - }); + describe('aspectRatio', () => { it('calculates a width / height aspectRatio', () => { expect(instance.aspectRatio).toBeCloseTo(0.667); diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js index aa207f7bde1fe0c5ba8b9ff7fb34f2f0b0db0746..87aa5094315b8482e3155f3f690f7bf7aa449c4b 100644 --- a/__tests__/src/lib/MiradorViewer.test.js +++ b/__tests__/src/lib/MiradorViewer.test.js @@ -5,17 +5,6 @@ jest.unmock('react-i18next'); jest.mock('react-dom'); jest.mock('isomorphic-unfetch', () => jest.fn(() => Promise.resolve({ json: () => ({}) }))); -jest.mock('../../../src/state/selectors', () => ({ - getCanvasGrouping: () => [], - getCompanionWindowIdsForPosition: () => ['cwid'], - getManifestoInstance: () => {}, - getManifests: () => ( - { 'https://iiif.harvardartmuseums.org/manifests/object/299843': { isFetching: true } } - ), - getManifestSearchService: () => ({ id: 'http://example.com/search' }), - getSearchForWindow: () => {}, -})); - describe('MiradorViewer', () => { let instance; beforeAll(() => { @@ -61,7 +50,7 @@ describe('MiradorViewer', () => { expect(windows[windowIds[0]].layoutOrder).toBe(0); expect(windows[windowIds[1]].layoutOrder).toBe(1); expect(windows[windowIds[0]].thumbnailNavigationPosition).toBe('far-bottom'); - expect(windows[windowIds[1]].thumbnailNavigationPosition).toBe('off'); + expect(windows[windowIds[1]].thumbnailNavigationPosition).toBe(undefined); expect(windows[windowIds[0]].view).toBe(undefined); expect(windows[windowIds[1]].view).toBe('book'); diff --git a/__tests__/src/sagas/annotations.test.js b/__tests__/src/sagas/annotations.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d99249da96145fb42eb5d3fbaaa1c7973312261a --- /dev/null +++ b/__tests__/src/sagas/annotations.test.js @@ -0,0 +1,74 @@ +import { select } from 'redux-saga/effects'; +import { expectSaga } from 'redux-saga-test-plan'; + +import { fetchAnnotations } from '../../../src/state/sagas/annotations'; +import { getAnnotations, getCanvases } from '../../../src/state/selectors'; + +describe('annotation sagas', () => { + describe('fetchAnnotations', () => { + it('requests IIIF v2-style annotations for each visible canvas', () => { + const action = { + visibleCanvases: ['a', 'b'], + windowId: 'foo', + }; + + return expectSaga(fetchAnnotations, action) + .provide([ + [select(getCanvases, { windowId: 'foo' }), [ + { __jsonld: { otherContent: 'annoId' }, id: 'a' }, + { __jsonld: { otherContent: ['alreadyFetched'] }, id: 'b' }, + ]], + [select(getAnnotations), { a: {}, b: { alreadyFetched: {} } }], + ]) + .put({ + annotationId: 'annoId', + targetId: 'a', + type: 'mirador/REQUEST_ANNOTATION', + }) + .run(); + }); + it('requests IIIF v3-style annotations for each visible canvas', () => { + const action = { + visibleCanvases: ['a', 'b'], + windowId: 'foo', + }; + + return expectSaga(fetchAnnotations, action) + .provide([ + [select(getCanvases, { windowId: 'foo' }), [ + { __jsonld: { annotations: { id: 'annoId', type: 'AnnotationPage' } }, id: 'a' }, + ]], + [select(getAnnotations), { a: {} }], + ]) + .put({ + annotationId: 'annoId', + targetId: 'a', + type: 'mirador/REQUEST_ANNOTATION', + }) + .run(); + }); + it('handles embedded IIIF v3-style annotations on each visible canvas', () => { + const action = { + visibleCanvases: ['a', 'b'], + windowId: 'foo', + }; + + const annotations = { id: 'annoId', items: [], type: 'AnnotationPage' }; + + return expectSaga(fetchAnnotations, action) + .provide([ + [select(getCanvases, { windowId: 'foo' }), [ + { __jsonld: { annotations }, id: 'a' }, + ]], + [select(getAnnotations), { a: {} }], + ]) + .put({ + annotationId: 'annoId', + annotationJson: annotations, + targetId: 'a', + type: 'mirador/RECEIVE_ANNOTATION', + }) + .run(); + }); + }); +}); diff --git a/__tests__/src/sagas/app.test.js b/__tests__/src/sagas/app.test.js index 6ce748e340ad3932d03d6049f5a87d496946007e..807e401cea53e8e28651c4b580b79613b93ea760 100644 --- a/__tests__/src/sagas/app.test.js +++ b/__tests__/src/sagas/app.test.js @@ -1,9 +1,10 @@ import { call } from 'redux-saga/effects'; -import { testSaga } from 'redux-saga-test-plan'; +import { expectSaga, testSaga } from 'redux-saga-test-plan'; -import { importState } from '../../../src/state/sagas/app'; +import { importConfig, importState } from '../../../src/state/sagas/app'; import { fetchManifest } from '../../../src/state/sagas/iiif'; import { fetchWindowManifest } from '../../../src/state/sagas/windows'; +import { addWindow } from '../../../src/state/actions'; describe('app-level sagas', () => { describe('importState', () => { @@ -52,4 +53,31 @@ describe('app-level sagas', () => { .all([]); }); }); + + describe('importConfig', () => { + it('adds windows from the provided config', () => { + const action = { + config: { + thumbnailNavigation: {}, + windows: [ + { id: 'x', manifestId: 'a' }, + { id: 'y', manifestId: 'b' }, + ], + }, + }; + + return expectSaga(importConfig, action) + .provide([ + [call(addWindow, { + id: 'x', layoutOrder: 0, manifestId: 'a', thumbnailNavigationPosition: undefined, + }), { type: 'thunk1' }], + [call(addWindow, { + id: 'y', layoutOrder: 1, manifestId: 'b', thumbnailNavigationPosition: undefined, + }), { type: 'thunk2' }], + ]) + .put({ type: 'thunk1' }) + .put({ type: 'thunk2' }) + .run(); + }); + }); }); diff --git a/__tests__/src/sagas/windows.test.js b/__tests__/src/sagas/windows.test.js index b557fda944ebf815500d18394bbfdd6fcb2bb84a..772051fa976ff57e3b80b9ecf104fd4f461ddc3f 100644 --- a/__tests__/src/sagas/windows.test.js +++ b/__tests__/src/sagas/windows.test.js @@ -13,6 +13,7 @@ import { getSelectedContentSearchAnnotationIds, getSortedSearchAnnotationsForCompanionWindow, getVisibleCanvasIds, getCanvasForAnnotation, + getCanvases, selectInfoResponses, } from '../../../src/state/selectors'; import { fetchManifest } from '../../../src/state/sagas/iiif'; import { @@ -25,6 +26,7 @@ import { setCanvasforSelectedAnnotation, panToFocusedWindow, setCurrentAnnotationsOnCurrentCanvas, + fetchInfoResponses, } from '../../../src/state/sagas/windows'; import fixture from '../../fixtures/version-2/019.json'; @@ -398,4 +400,50 @@ describe('window-level sagas', () => { .run().then(({ allEffects }) => allEffects.length === 0); }); }); + + describe('fetchInfoResponses', () => { + it('requests info responses for each visible canvas', () => { + const action = { + visibleCanvases: ['http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'], + windowId: 'foo', + }; + + const manifest = Utils.parseManifest(fixture); + + return expectSaga(fetchInfoResponses, action) + .provide([ + [select(getCanvases, { windowId: 'foo' }), manifest.getSequences()[0].getCanvases()], + [select(selectInfoResponses), {}], + ]) + .put.like({ + action: { + infoId: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44', + type: 'mirador/REQUEST_INFO_RESPONSE', + }, + }) + .run(); + }); + + it('requests nothing if the response is already in the store', () => { + const action = { + visibleCanvases: ['http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'], + windowId: 'foo', + }; + + const manifest = Utils.parseManifest(fixture); + + return expectSaga(fetchInfoResponses, action) + .provide([ + [select(getCanvases, { windowId: 'foo' }), manifest.getSequences()[0].getCanvases()], + [select(selectInfoResponses), { 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44': {} }], + ]) + .not.put.like({ + action: { + infoId: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44', + type: 'mirador/REQUEST_INFO_RESPONSE', + }, + }) + .run(); + }); + }); }); diff --git a/package.json b/package.json index 5f71f471357475b32d43928315953927e58cae48..12e3b216deb6037f552f0c8358b108d65caf5205 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "redux": "4.0.1", "redux-devtools-extension": "^2.13.2", "redux-saga": "^1.1.3", - "redux-saga-test-plan": "^4.0.0-rc.3", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "uuid": "^8.1.0" @@ -120,6 +119,7 @@ "react-dev-utils": "^9.0.1", "react-dom": "^16.8.6", "redux-mock-store": "^1.5.1", + "redux-saga-test-plan": "^4.0.0-rc.3", "style-loader": "^0.23.1", "supertest": "^4.0.2", "terser-webpack-plugin": "^1.3.0", diff --git a/src/components/WindowViewer.js b/src/components/WindowViewer.js index 995e5f1fe86f0010bbaa93e1b321c4650d2e45dd..7a7d1acc697ea00338b6ce34db34dad0c2316cb9 100644 --- a/src/components/WindowViewer.js +++ b/src/components/WindowViewer.js @@ -1,10 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import difference from 'lodash/difference'; -import flatten from 'lodash/flatten'; import OSDViewer from '../containers/OpenSeadragonViewer'; import WindowCanvasNavigationControls from '../containers/WindowCanvasNavigationControls'; -import MiradorCanvas from '../lib/MiradorCanvas'; /** * Represents a WindowViewer in the mirador workspace. Responsible for mounting @@ -23,93 +20,6 @@ export class WindowViewer extends Component { return { hasError: true }; } - /** - * componentDidMount - React lifecycle method - * Request the initial canvas on mount - */ - componentDidMount() { - const { - currentCanvases, fetchInfoResponse, fetchAnnotation, receiveAnnotation, - } = this.props; - - if (!this.infoResponseIsInStore()) { - currentCanvases.forEach((canvas) => { - const miradorCanvas = new MiradorCanvas(canvas); - miradorCanvas.iiifImageResources.forEach((imageResource) => { - fetchInfoResponse({ imageResource }); - }); - miradorCanvas.processAnnotations(fetchAnnotation, receiveAnnotation); - }); - } - } - - /** - * componentDidUpdate - React lifecycle method - * Request a new canvas if it is needed - */ - componentDidUpdate(prevProps) { - const { - currentCanvases, fetchInfoResponse, fetchAnnotation, receiveAnnotation, - } = this.props; - - if (difference(currentCanvases, prevProps.currentCanvases).length > 0 - && !this.infoResponseIsInStore()) { - currentCanvases.forEach((canvas) => { - const miradorCanvas = new MiradorCanvas(canvas); - miradorCanvas.iiifImageResources.forEach((imageResource) => { - fetchInfoResponse({ imageResource }); - }); - miradorCanvas.processAnnotations(fetchAnnotation, receiveAnnotation); - }); - } - } - - /** - * infoResponseIsInStore - checks whether or not an info response is already - * in the store. No need to request it again. - * @return [Boolean] - */ - infoResponseIsInStore() { - const responses = this.currentInfoResponses(); - if (responses.length === this.imageServiceIds().length) { - return true; - } - return false; - } - - /** */ - imageServiceIds() { - const { currentCanvases } = this.props; - - return flatten(currentCanvases.map(canvas => new MiradorCanvas(canvas).imageServiceIds)); - } - - /** - * currentInfoResponses - Selects infoResponses that are relevent to existing - * canvases to be displayed. - */ - currentInfoResponses() { - const { infoResponses } = this.props; - - return this.imageServiceIds().map(imageId => ( - infoResponses[imageId] - )).filter(infoResponse => (infoResponse !== undefined - && infoResponse.isFetching === false - && infoResponse.error === undefined)); - } - - /** - * Return an image information response from the store for the correct image - */ - infoResponsesFetchedFromStore() { - const responses = this.currentInfoResponses(); - // Only return actual tileSources when all current canvases have completed. - if (responses.length === this.imageServiceIds().length) { - return responses; - } - return []; - } - /** * Renders things */ @@ -124,20 +34,14 @@ export class WindowViewer extends Component { return ( <OSDViewer - infoResponses={this.infoResponsesFetchedFromStore()} windowId={windowId} > - <WindowCanvasNavigationControls key="canvas_nav" windowId={windowId} /> + <WindowCanvasNavigationControls windowId={windowId} /> </OSDViewer> ); } } WindowViewer.propTypes = { - currentCanvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types - fetchAnnotation: PropTypes.func.isRequired, - fetchInfoResponse: PropTypes.func.isRequired, - infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - receiveAnnotation: PropTypes.func.isRequired, windowId: PropTypes.string.isRequired, }; diff --git a/src/containers/OpenSeadragonViewer.js b/src/containers/OpenSeadragonViewer.js index 96faa482966383f25357f297faa3ed8742b7e7b7..c733eb33bce2be58a22f31805de13239a1ca870c 100644 --- a/src/containers/OpenSeadragonViewer.js +++ b/src/containers/OpenSeadragonViewer.js @@ -2,6 +2,7 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; import { withStyles } from '@material-ui/core/styles'; +import flatten from 'lodash/flatten'; import { withPlugins } from '../extend/withPlugins'; import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer'; import * as actions from '../state/actions'; @@ -14,6 +15,7 @@ import { getViewer, getConfig, getCompanionWindowsForContent, + selectInfoResponses, } from '../state/selectors'; /** @@ -21,19 +23,30 @@ import { * @memberof Window * @private */ -const mapStateToProps = (state, { windowId }) => ({ - canvasWorld: new CanvasWorld( +const mapStateToProps = (state, { windowId }) => { + const canvasWorld = new CanvasWorld( getVisibleCanvases(state, { windowId }), getLayersForVisibleCanvases(state, { windowId }), getSequenceViewingDirection(state, { windowId }), - ), - drawAnnotations: getConfig(state).window.forceDrawAnnotations - || getCompanionWindowsForContent(state, { content: 'annotations', windowId }).length > 0 - || getCompanionWindowsForContent(state, { content: 'search', windowId }).length > 0, - nonTiledImages: getVisibleCanvasNonTiledResources(state, { windowId }), - osdConfig: state.config.osdConfig, - viewerConfig: getViewer(state, { windowId }), -}); + ); + + const infoResponses = selectInfoResponses(state); + const imageServiceIds = flatten(canvasWorld.canvases.map(c => c.imageServiceIds)); + + return { + canvasWorld, + drawAnnotations: getConfig(state).window.forceDrawAnnotations + || getCompanionWindowsForContent(state, { content: 'annotations', windowId }).length > 0 + || getCompanionWindowsForContent(state, { content: 'search', windowId }).length > 0, + infoResponses: imageServiceIds.map(id => infoResponses[id]) + .filter(infoResponse => (infoResponse !== undefined + && infoResponse.isFetching === false + && infoResponse.error === undefined)), + nonTiledImages: getVisibleCanvasNonTiledResources(state, { windowId }), + osdConfig: state.config.osdConfig, + viewerConfig: getViewer(state, { windowId }), + }; +}; /** * mapDispatchToProps - used to hook up connect to action creators diff --git a/src/containers/WindowViewer.js b/src/containers/WindowViewer.js index 1a687a388ab22f2532ee074ccdda52bd47893a58..d61c69c21f37a6179a505e954e7607aef4fb909a 100644 --- a/src/containers/WindowViewer.js +++ b/src/containers/WindowViewer.js @@ -3,19 +3,13 @@ import { connect } from 'react-redux'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { WindowViewer } from '../components/WindowViewer'; -import { getVisibleCanvases } from '../state/selectors'; /** * mapStateToProps - to hook up connect * @memberof WindowViewer * @private */ -const mapStateToProps = (state, { windowId }) => ( - { - currentCanvases: getVisibleCanvases(state, { windowId }) || [], - infoResponses: state.infoResponses, - } -); +const mapStateToProps = (state, { windowId }) => ({}); /** * mapDispatchToProps - used to hook up connect to action creators diff --git a/src/lib/MiradorCanvas.js b/src/lib/MiradorCanvas.js index 9515320efe92c59fec484ee6e4122915a9aa318f..0c69710307c16e841abfcbcea260bd389e4219a1 100644 --- a/src/lib/MiradorCanvas.js +++ b/src/lib/MiradorCanvas.js @@ -57,24 +57,6 @@ export default class MiradorCanvas { .filter(annotations => annotations && annotations.type === 'AnnotationPage'); } - /** */ - processAnnotations(fetchAnnotation, receiveAnnotation) { - // IIIF v2 - this.annotationListUris.forEach((uri) => { - fetchAnnotation(this.canvas.id, uri); - }); - // IIIF v3 - this.canvasAnnotationPages.forEach((annotation) => { - // If there are no items, try to retrieve the referenced resource. - // otherwise the resource should be embedded and just add to the store. - if (!annotation.items) { - fetchAnnotation(this.canvas.id, annotation.id); - } else { - receiveAnnotation(this.canvas.id, annotation.id, annotation); - } - }); - } - /** * Will negotiate a v2 or v3 type of resource */ diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js index cf54c62b47ccac3d5a7d67f542a20f39748bc6d3..81346abbab0e717046c45899fa426ed8f6570630 100644 --- a/src/lib/MiradorViewer.js +++ b/src/lib/MiradorViewer.js @@ -1,7 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import { v4 as uuid } from 'uuid'; import HotApp from '../components/App'; import createStore from '../state/createStore'; import * as actions from '../state/actions'; @@ -46,22 +45,6 @@ class MiradorViewer { /** merge type for arrays */ const action = actions.importConfig(this.config); this.store.dispatch(action); - const { config: storedConfig } = this.store.getState(); - - storedConfig.windows.forEach((miradorWindow, layoutOrder) => { - const windowId = `window-${uuid()}`; - const manifestId = miradorWindow.manifestId || miradorWindow.loadedManifest; - - this.store.dispatch(actions.addWindow({ - // these are default values ... - id: windowId, - layoutOrder, - manifestId, - thumbnailNavigationPosition: storedConfig.thumbnailNavigation.defaultPosition, - // ... overridden by values from the window configuration ... - ...miradorWindow, - })); - }); } } diff --git a/src/state/actions/annotation.js b/src/state/actions/annotation.js index dd61cb0f325e16c9e426a2594b7aa99505f20487..1091d7e4a9b20236d4460bc19520b41296404838 100644 --- a/src/state/actions/annotation.js +++ b/src/state/actions/annotation.js @@ -49,16 +49,6 @@ export function receiveAnnotationFailure(targetId, annotationId, error) { }; } -/** - * fetchAnnotation - action creator - * - * @param {String} annotationId - * @memberof ActionCreators - */ -export function fetchAnnotation(targetId, annotationId) { - return requestAnnotation(targetId, annotationId); -} - /** * selectAnnotation - action creator * diff --git a/src/state/sagas/annotations.js b/src/state/sagas/annotations.js new file mode 100644 index 0000000000000000000000000000000000000000..885c19ead77f0ec627a4f9d87fb2ff5d3145891e --- /dev/null +++ b/src/state/sagas/annotations.js @@ -0,0 +1,45 @@ +import { + all, put, select, takeEvery, +} from 'redux-saga/effects'; +import { receiveAnnotation, requestAnnotation } from '../actions'; +import { getAnnotations, getCanvases } from '../selectors'; +import ActionTypes from '../actions/action-types'; +import MiradorCanvas from '../../lib/MiradorCanvas'; + +/** Fetch annotations for the visible canvases */ +export function* fetchAnnotations({ visibleCanvases: visibleCanvasIds, windowId }) { + const canvases = yield select(getCanvases, { windowId }); + const visibleCanvases = (canvases || []).filter(c => visibleCanvasIds.includes(c.id)); + + const annotations = yield select(getAnnotations); + + yield all(visibleCanvases.map((canvas) => { + const miradorCanvas = new MiradorCanvas(canvas); + + return all([ + // IIIF v2 + ...miradorCanvas.annotationListUris + .filter(uri => !(annotations[canvas.id] && annotations[canvas.id][uri])) + .map(uri => put(requestAnnotation(canvas.id, uri))), + // IIIF v3 + ...miradorCanvas.canvasAnnotationPages + .filter(annotation => !(annotations[canvas.id] && annotations[canvas.id][annotation.id])) + .map((annotation) => { + // If there are no items, try to retrieve the referenced resource. + // otherwise the resource should be embedded and just add to the store. + if (!annotation.items) { + return put(requestAnnotation(canvas.id, annotation.id)); + } + + return put(receiveAnnotation(canvas.id, annotation.id, annotation)); + }), + ]); + })); +} + +/** */ +export default function* appSaga() { + yield all([ + takeEvery(ActionTypes.SET_CANVAS, fetchAnnotations), + ]); +} diff --git a/src/state/sagas/app.js b/src/state/sagas/app.js index 08de29310c748eec72c9f80ac506159460a918b1..de44905423376b3cc3b5a212f2d502fe34e2a981 100644 --- a/src/state/sagas/app.js +++ b/src/state/sagas/app.js @@ -1,8 +1,10 @@ import { - all, call, takeEvery, + all, call, put, takeEvery, } from 'redux-saga/effects'; +import { v4 as uuid } from 'uuid'; import { fetchManifest } from './iiif'; import { fetchWindowManifest } from './windows'; +import { addWindow } from '../actions'; import ActionTypes from '../actions/action-types'; /** */ @@ -16,9 +18,34 @@ export function* importState(action) { ]); } +/** Add windows from the imported config */ +export function* importConfig({ config: { thumbnailNavigation, windows } }) { + if (!windows || windows.length === 0) return; + + const thunks = yield all( + windows.map((miradorWindow, layoutOrder) => { + const windowId = `window-${uuid()}`; + const manifestId = miradorWindow.manifestId || miradorWindow.loadedManifest; + + return call(addWindow, { + // these are default values ... + id: windowId, + layoutOrder, + manifestId, + thumbnailNavigationPosition: thumbnailNavigation && thumbnailNavigation.defaultPosition, + // ... overridden by values from the window configuration ... + ...miradorWindow, + }); + }), + ); + + yield all(thunks.map(thunk => put(thunk))); +} + /** */ export default function* appSaga() { yield all([ takeEvery(ActionTypes.IMPORT_MIRADOR_STATE, importState), + takeEvery(ActionTypes.IMPORT_CONFIG, importConfig), ]); } diff --git a/src/state/sagas/index.js b/src/state/sagas/index.js index fb13462ab9f96cfbff96ee4119e06510901bd936..63ff1cac0bbe1811ed27ed1c2ae34cc16ed2a52a 100644 --- a/src/state/sagas/index.js +++ b/src/state/sagas/index.js @@ -5,6 +5,7 @@ import { import appSaga from './app'; import iiifSaga from './iiif'; import windowSaga from './windows'; +import annotations from './annotations'; /** */ function* launchSaga(saga) { @@ -22,6 +23,7 @@ function* launchSaga(saga) { function getRootSaga(pluginSagas) { return function* rootSaga() { const sagas = [ + annotations, appSaga, iiifSaga, windowSaga, diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js index f25da8822aff67334f43643f6d2f9fc47d1eb7a0..656a9f0947511fbb7a1dc3e69615180e1b57451c 100644 --- a/src/state/sagas/windows.js +++ b/src/state/sagas/windows.js @@ -3,6 +3,7 @@ import { } from 'redux-saga/effects'; import ActionTypes from '../actions/action-types'; import MiradorManifest from '../../lib/MiradorManifest'; +import MiradorCanvas from '../../lib/MiradorCanvas'; import { setContentSearchCurrentAnnotation, selectAnnotation, @@ -11,6 +12,7 @@ import { setCanvas, fetchSearch, receiveManifest, + fetchInfoResponse, } from '../actions'; import { getSearchForWindow, getSearchAnnotationsForCompanionWindow, @@ -22,6 +24,8 @@ import { getVisibleCanvasIds, getWorkspace, getElasticLayout, + getCanvases, + selectInfoResponses, } from '../selectors'; import { fetchManifest } from './iiif'; @@ -183,12 +187,28 @@ export function* setCanvasforSelectedAnnotation({ annotationId, windowId }) { yield put(thunk); } +/** Fetch info responses for the visible canvases */ +export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowId }) { + const canvases = yield select(getCanvases, { windowId }); + const infoResponses = yield select(selectInfoResponses); + const visibleCanvases = (canvases || []).filter(c => visibleCanvasIds.includes(c.id)); + + yield all(visibleCanvases.map((canvas) => { + const miradorCanvas = new MiradorCanvas(canvas); + return all(miradorCanvas.iiifImageResources.map(imageResource => ( + !infoResponses[imageResource.getServices()[0].id] + && put(fetchInfoResponse({ imageResource })) + )).filter(Boolean)); + })); +} + /** */ export default function* windowsSaga() { yield all([ takeEvery(ActionTypes.ADD_WINDOW, fetchWindowManifest), takeEvery(ActionTypes.UPDATE_WINDOW, fetchWindowManifest), takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas), + takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses), takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases), takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult), takeEvery(ActionTypes.SELECT_ANNOTATION, setCanvasforSelectedAnnotation), diff --git a/src/state/selectors/annotations.js b/src/state/selectors/annotations.js index c80ba04fc5e06ee7b87d98aeb769124978715481..7fec25ef117cbd398c271ee803481b627ddd619f 100644 --- a/src/state/selectors/annotations.js +++ b/src/state/selectors/annotations.js @@ -4,10 +4,13 @@ import flatten from 'lodash/flatten'; import AnnotationFactory from '../../lib/AnnotationFactory'; import { getCanvas, getVisibleCanvasIds } from './canvases'; +/** */ +export const getAnnotations = state => state.annotations; + const getAnnotationsOnCanvas = createSelector( [ getCanvas, - state => state.annotations, + getAnnotations, ], (canvas, annotations) => { if (!annotations || !canvas) return [];