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