From 382733454f8f99d42df1a99390047b4c3ff19c1b Mon Sep 17 00:00:00 2001 From: Jack Reed <phillipjreed@gmail.com> Date: Fri, 1 Mar 2019 10:32:46 -0700 Subject: [PATCH] implement basic annotation state referenced by canvasId --- __tests__/src/actions/annotation.test.js | 110 +++++++++++++++++++++ __tests__/src/reducers/annotations.test.js | 75 ++++++++++++++ src/state/actions/action-types.js | 4 + src/state/actions/annotation.js | 67 +++++++++++++ src/state/actions/index.js | 1 + src/state/reducers/annotations.js | 42 ++++++++ src/state/reducers/index.js | 1 + src/state/reducers/rootReducer.js | 2 + 8 files changed, 302 insertions(+) create mode 100644 __tests__/src/actions/annotation.test.js create mode 100644 __tests__/src/reducers/annotations.test.js create mode 100644 src/state/actions/annotation.js create mode 100644 src/state/reducers/annotations.js diff --git a/__tests__/src/actions/annotation.test.js b/__tests__/src/actions/annotation.test.js new file mode 100644 index 000000000..97632ae57 --- /dev/null +++ b/__tests__/src/actions/annotation.test.js @@ -0,0 +1,110 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import * as actions from '../../../src/state/actions'; +import ActionTypes from '../../../src/state/actions/action-types'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('annotation actions', () => { + describe('requestAnnotation', () => { + it('requests an annotation from given a url', () => { + const canvasId = 'foo'; + const annotationId = 'abc123'; + const expectedAction = { + type: ActionTypes.REQUEST_ANNOTATION, + canvasId, + annotationId, + }; + expect(actions.requestAnnotation(canvasId, annotationId)).toEqual(expectedAction); + }); + }); + describe('receiveAnnotation', () => { + it('recieves an annotation', () => { + const canvasId = 'foo'; + const annotationId = 'abc123'; + const json = { + annotationId, + content: 'annotation request', + }; + const expectedAction = { + type: ActionTypes.RECEIVE_ANNOTATION, + canvasId, + annotationId, + annotationJson: json, + }; + expect(actions.receiveAnnotation(canvasId, annotationId, json)).toEqual(expectedAction); + }); + }); + describe('fetchAnnotation', () => { + let store = null; + beforeEach(() => { + store = mockStore({}); + }); + describe('success response', () => { + beforeEach(() => { + fetch.mockResponseOnce(JSON.stringify({ data: '12345' })); // eslint-disable-line no-undef + }); + it('dispatches the REQUEST_ANNOTATION action', () => { + store.dispatch(actions.fetchAnnotation( + 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + )); + expect(store.getActions()).toEqual([ + { + canvasId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + annotationId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + type: 'REQUEST_ANNOTATION', + }, + ]); + }); + it('dispatches the REQUEST_ANNOTATION and then RECEIVE_ANNOTATION', () => { + store.dispatch(actions.fetchAnnotation( + 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + )) + .then(() => { + const expectedActions = store.getActions(); + expect(expectedActions).toEqual([ + { + canvasId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + annotationId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + type: 'REQUEST_ANNOTATION', + }, + { + canvasId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + annotationId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + annotationJson: { data: '12345' }, + type: 'RECEIVE_ANNOTATION', + }, + ]); + }); + }); + }); + describe('error response', () => { + it('dispatches the REQUEST_ANNOTATION and then RECEIVE_ANNOTATION', () => { + store.dispatch(actions.fetchAnnotation( + 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + )) + .then(() => { + const expectedActions = store.getActions(); + expect(expectedActions).toEqual([ + { + canvasId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + annotationId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + type: 'REQUEST_ANNOTATION', + }, + { + canvasId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896', + annotationId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896', + error: new Error('invalid json response body at undefined reason: Unexpected end of JSON input'), + type: 'RECEIVE_ANNOTATION_FAILURE', + }, + ]); + }); + }); + }); + }); +}); diff --git a/__tests__/src/reducers/annotations.test.js b/__tests__/src/reducers/annotations.test.js new file mode 100644 index 000000000..5b5479925 --- /dev/null +++ b/__tests__/src/reducers/annotations.test.js @@ -0,0 +1,75 @@ +import { annotationsReducer } from '../../../src/state/reducers/annotations'; +import ActionTypes from '../../../src/state/actions/action-types'; + +describe('annotation reducer', () => { + it('should handle REQUEST_ANNOTATION', () => { + expect(annotationsReducer({}, { + type: ActionTypes.REQUEST_ANNOTATION, + canvasId: 'foo', + annotationId: 'abc123', + })).toEqual({ + foo: { + abc123: { + id: 'abc123', + isFetching: true, + }, + }, + }); + }); + it('should handle RECEIVE_ANNOTATION', () => { + expect(annotationsReducer( + { + foo: { + abc123: { + id: 'abc123', + isFetching: true, + }, + }, + }, + { + type: ActionTypes.RECEIVE_ANNOTATION, + canvasId: 'foo', + annotationId: 'abc123', + annotationJson: { + id: 'abc123', + '@type': 'sc:AnnotationList', + content: 'anno stuff', + }, + }, + )).toMatchObject({ + foo: { + abc123: { + id: 'abc123', + isFetching: false, + json: {}, + }, + }, + }); + }); + it('should handle RECEIVE_ANNOTATION_FAILURE', () => { + expect(annotationsReducer( + { + foo: { + abc123: { + id: 'abc123', + isFetching: true, + }, + }, + }, + { + type: ActionTypes.RECEIVE_ANNOTATION_FAILURE, + canvasId: 'foo', + annotationId: 'abc123', + error: "This institution didn't enable CORS.", + }, + )).toEqual({ + foo: { + abc123: { + id: 'abc123', + isFetching: false, + error: "This institution didn't enable CORS.", + }, + }, + }); + }); +}); diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js index e6ebd50c6..36857f308 100644 --- a/src/state/actions/action-types.js +++ b/src/state/actions/action-types.js @@ -4,6 +4,10 @@ const ActionTypes = { REMOVE_COMPANION_WINDOW: 'REMOVE_COMPANION_WINDOW', UPDATE_WINDOW: 'UPDATE_WINDOW', + REQUEST_ANNOTATION: 'REQUEST_ANNOTATION', + RECEIVE_ANNOTATION: 'RECEIVE_ANNOTATION', + RECEIVE_ANNOTATION_FAILURE: 'RECEIVE_ANNOTATION_FAILURE', + FOCUS_WINDOW: 'FOCUS_WINDOW', SET_WORKSPACE_FULLSCREEN: 'SET_WORKSPACE_FULLSCREEN', SET_WORKSPACE_VIEWPORT_POSITION: 'SET_WORKSPACE_VIEWPORT_POSITION', diff --git a/src/state/actions/annotation.js b/src/state/actions/annotation.js new file mode 100644 index 000000000..4924945b0 --- /dev/null +++ b/src/state/actions/annotation.js @@ -0,0 +1,67 @@ +import fetch from 'node-fetch'; +import ActionTypes from './action-types'; + +/** + * requestAnnotation - action creator + * + * @param {String} canvasId + * @param {String} annotationId + * @memberof ActionCreators + */ +export function requestAnnotation(canvasId, annotationId) { + return { + type: ActionTypes.REQUEST_ANNOTATION, + canvasId, + annotationId, + }; +} + +/** + * receiveAnnotation - action creator + * + * @param {String} canvasId + * @param {String} annotationId + * @param {Object} annotationJson + * @memberof ActionCreators + */ +export function receiveAnnotation(canvasId, annotationId, annotationJson) { + return { + type: ActionTypes.RECEIVE_ANNOTATION, + canvasId, + annotationId, + annotationJson, + }; +} + +/** + * receiveAnnotationFailure - action creator + * + * @param {String} canvasId + * @param {String} annotationId + * @param {String} error + * @memberof ActionCreators + */ +export function receiveAnnotationFailure(canvasId, annotationId, error) { + return { + type: ActionTypes.RECEIVE_ANNOTATION_FAILURE, + canvasId, + annotationId, + error, + }; +} + +/** + * fetchAnnotation - action creator + * + * @param {String} annotationId + * @memberof ActionCreators + */ +export function fetchAnnotation(canvasId, annotationId) { + return ((dispatch) => { + dispatch(requestAnnotation(canvasId, annotationId)); + return fetch(annotationId) + .then(response => response.json()) + .then(json => dispatch(receiveAnnotation(canvasId, annotationId, json))) + .catch(error => dispatch(receiveAnnotationFailure(canvasId, annotationId, error))); + }); +} diff --git a/src/state/actions/index.js b/src/state/actions/index.js index 465e1fd28..9465bc715 100644 --- a/src/state/actions/index.js +++ b/src/state/actions/index.js @@ -9,3 +9,4 @@ export * from './manifest'; export * from './infoResponse'; export * from './canvas'; export * from './workspace'; +export * from './annotation'; diff --git a/src/state/reducers/annotations.js b/src/state/reducers/annotations.js new file mode 100644 index 000000000..140b68bb7 --- /dev/null +++ b/src/state/reducers/annotations.js @@ -0,0 +1,42 @@ +import ActionTypes from '../actions/action-types'; + +/** + * annotationReducer + */ +export const annotationsReducer = (state = {}, action) => { + switch (action.type) { + case ActionTypes.REQUEST_ANNOTATION: + return { + ...state, + [action.canvasId]: { + [action.annotationId]: { + id: action.annotationId, + isFetching: true, + }, + }, + }; + case ActionTypes.RECEIVE_ANNOTATION: + return { + ...state, + [action.canvasId]: { + [action.annotationId]: { + id: action.annotationId, + json: action.annotationJson, + isFetching: false, + }, + }, + }; + case ActionTypes.RECEIVE_ANNOTATION_FAILURE: + return { + ...state, + [action.canvasId]: { + [action.annotationId]: { + id: action.annotationId, + error: action.error, + isFetching: false, + }, + }, + }; + default: return state; + } +}; diff --git a/src/state/reducers/index.js b/src/state/reducers/index.js index 478ac4467..481f80824 100644 --- a/src/state/reducers/index.js +++ b/src/state/reducers/index.js @@ -5,3 +5,4 @@ export * from './manifests'; export * from './infoResponses'; export * from './config'; export * from './viewers'; +export * from './annotations'; diff --git a/src/state/reducers/rootReducer.js b/src/state/reducers/rootReducer.js index 927420a4f..087b0024c 100644 --- a/src/state/reducers/rootReducer.js +++ b/src/state/reducers/rootReducer.js @@ -7,6 +7,7 @@ import { viewersReducer, windowsReducer, workspaceReducer, + annotationsReducer, } from '.'; /** @@ -23,6 +24,7 @@ export default function createRootReducer(pluginReducers) { infoResponses: infoResponsesReducer, config: configReducer, viewers: viewersReducer, + annotations: annotationsReducer, ...pluginReducers, }); } -- GitLab