From 7bebf9964d40754dda7a65262d2e7b4f4291fb91 Mon Sep 17 00:00:00 2001
From: Jack Reed <phillipjreed@gmail.com>
Date: Wed, 20 May 2020 17:31:04 -0600
Subject: [PATCH] Display annotation tags in CanvasAnnotation list fixes #3051

---
 __tests__/fixtures/version-3/001.json         | 41 ++++++++++++++++---
 .../src/components/CanvasAnnotations.test.js  | 13 ++++++
 __tests__/src/lib/AnnotationItem.test.js      | 27 ++++++++++++
 __tests__/src/lib/AnnotationResource.test.js  | 27 ++++++++++++
 src/components/CanvasAnnotations.js           |  8 ++++
 src/config/settings.js                        |  2 +-
 src/containers/CanvasAnnotations.js           |  6 +++
 src/lib/AnnotationItem.js                     | 16 +++++++-
 src/lib/AnnotationResource.js                 | 15 ++++++-
 9 files changed, 147 insertions(+), 8 deletions(-)

diff --git a/__tests__/fixtures/version-3/001.json b/__tests__/fixtures/version-3/001.json
index c43fab55e..a148c32bc 100644
--- a/__tests__/fixtures/version-3/001.json
+++ b/__tests__/fixtures/version-3/001.json
@@ -286,11 +286,18 @@
               "target": "https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json#xywh=2000,500,2000,2000"
             },
             {
-              "body": {
-                "language": "en",
-                "type": "TextualBody",
-                "value": "this is a face"
-              },
+              "body": [
+                {
+                  "language": "en",
+                  "type": "TextualBody",
+                  "value": "this is a face"
+                },
+                {
+                  "type": "TextualBody",
+                  "value": "Face",
+                  "purpose": "tagging"
+                }
+              ],
               "id": "https://example.org/iiif/book1/page/manifest/9c6934ee-1026-4a10-8a97-aaf513513020",
               "motivation": "commenting",
               "target": {
@@ -301,6 +308,30 @@
                 }
               },
               "type": "Annotation"
+            },
+            {
+              "id": "https://example.org/iiif/book1/page/manifest/a3",
+              "type": "Annotation",
+              "motivation": "tagging",
+              "body": [
+                {
+                  "type": "TextualBody",
+                  "value": "Tree"
+                },
+                {
+                  "type": "TextualBody",
+                  "value": "Drawing"
+                },
+                {
+                  "type": "TextualBody",
+                  "value": "Coniferous"
+                },
+                {
+                  "type": "TextualBody",
+                  "value": "Stanford Cardinal"
+                }
+              ],
+              "target": "https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json#xywh=1605,961,437,625"
             }
           ]
         },
diff --git a/__tests__/src/components/CanvasAnnotations.test.js b/__tests__/src/components/CanvasAnnotations.test.js
index c8f8c318a..a3d0e5d7d 100644
--- a/__tests__/src/components/CanvasAnnotations.test.js
+++ b/__tests__/src/components/CanvasAnnotations.test.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import Typography from '@material-ui/core/Typography';
+import Chip from '@material-ui/core/Chip';
 import List from '@material-ui/core/List';
 import ListItem from '@material-ui/core/ListItem';
 import { CanvasAnnotations } from '../../../src/components/CanvasAnnotations';
@@ -30,11 +31,13 @@ describe('CanvasAnnotations', () => {
     {
       content: 'First Annotation',
       id: 'abc123',
+      tags: ['abc123', 'def456'],
       targetId: 'example.com/iiif/12345',
     },
     {
       content: 'Last Annotation',
       id: 'xyz321',
+      tags: [],
       targetId: 'example.com/iiif/54321',
     },
   ];
@@ -61,6 +64,12 @@ describe('CanvasAnnotations', () => {
     expect(wrapper.find(ListItem).length).toEqual(2);
   });
 
+  it('renders a Chip for every tag', () => {
+    wrapper = createWrapper({ annotations });
+
+    expect(wrapper.find(Chip).length).toEqual(2);
+  });
+
   it('pass through the annotation to make plugins life easier', () => {
     wrapper = createWrapper({ annotations });
     expect(wrapper.find(ListItem).first().dive().props().annotation.id).toEqual('abc123');
@@ -107,6 +116,7 @@ describe('CanvasAnnotations', () => {
             {
               content: 'Annotation',
               id: 'annoId',
+              tags: [],
               targetId: 'example.com/iiif/12345',
             },
           ],
@@ -130,6 +140,7 @@ describe('CanvasAnnotations', () => {
             {
               content: 'Annotation',
               id: 'annoId',
+              tags: [],
               targetId: 'example.com/iiif/12345',
             },
           ],
@@ -148,6 +159,7 @@ describe('CanvasAnnotations', () => {
             {
               content: 'Annotation',
               id: 'annoId',
+              tags: [],
               targetId: 'example.com/iiif/12345',
             },
           ],
@@ -166,6 +178,7 @@ describe('CanvasAnnotations', () => {
             {
               content: 'Annotation',
               id: 'annoId',
+              tags: [],
               targetId: 'example.com/iiif/12345',
             },
           ],
diff --git a/__tests__/src/lib/AnnotationItem.test.js b/__tests__/src/lib/AnnotationItem.test.js
index 995d7a555..bf8345e9f 100644
--- a/__tests__/src/lib/AnnotationItem.test.js
+++ b/__tests__/src/lib/AnnotationItem.test.js
@@ -12,6 +12,33 @@ describe('AnnotationItem', () => {
     });
   });
 
+  describe('isOnlyTag', () => {
+    it('when the only motivation is tagging', () => {
+      expect(new AnnotationItem({ motivation: 'tagging' }).isOnlyTag())
+        .toBe(true);
+    });
+    it('when there are other motivations besides tagging', () => {
+      expect(new AnnotationItem({ motivation: ['commenting', 'tagging'] }).isOnlyTag())
+        .toBe(false);
+    });
+  });
+
+  describe('tags', () => {
+    it('when only motivation', () => {
+      expect(
+        new AnnotationItem({ body: [{ purpose: 'tagging', value: 'yo' }, { purpose: 'tagging', value: 'lo' }] }).tags,
+      ).toEqual(['yo', 'lo']);
+    });
+    it('when multiple motivations', () => {
+      expect(
+        new AnnotationItem({
+          body: [{ purpose: 'commenting', value: 'yo' }, { purpose: 'tagging', value: 'lo' }],
+          motivation: ['commenting', 'tagging'],
+        }).tags,
+      ).toEqual(['lo']);
+    });
+  });
+
   describe('targetId', () => {
     it('removes fragmentSelector coords from string targets', () => {
       expect(
diff --git a/__tests__/src/lib/AnnotationResource.test.js b/__tests__/src/lib/AnnotationResource.test.js
index 08dc0a0d7..9fd5cb941 100644
--- a/__tests__/src/lib/AnnotationResource.test.js
+++ b/__tests__/src/lib/AnnotationResource.test.js
@@ -12,6 +12,33 @@ describe('AnnotationResource', () => {
     });
   });
 
+  describe('isOnlyTag', () => {
+    it('when the only motivation is tagging', () => {
+      expect(new AnnotationResource({ motivation: 'oa:tagging' }).isOnlyTag())
+        .toBe(true);
+    });
+    it('when there are other motivations besides tagging', () => {
+      expect(new AnnotationResource({ motivation: ['oa:commenting', 'oa:tagging'] }).isOnlyTag())
+        .toBe(false);
+    });
+  });
+
+  describe('tags', () => {
+    it('when only motivation', () => {
+      expect(
+        new AnnotationResource({ resource: [{ '@type': 'oa:Tag', value: 'yo' }, { '@type': 'oa:Tag', value: 'lo' }] }).tags,
+      ).toEqual(['yo', 'lo']);
+    });
+    it('when multiple motivations', () => {
+      expect(
+        new AnnotationResource({
+          motivation: ['oa:commenting', 'oa:tagging'],
+          resource: [{ '@type': 'oa:commenting', value: 'yo' }, { '@type': 'oa:Tag', value: 'lo' }],
+        }).tags,
+      ).toEqual(['lo']);
+    });
+  });
+
   describe('targetId', () => {
     it('removes fragmentSelector coords from string targets', () => {
       expect(
diff --git a/src/components/CanvasAnnotations.js b/src/components/CanvasAnnotations.js
index 5e3af2391..a55e4f52a 100644
--- a/src/components/CanvasAnnotations.js
+++ b/src/components/CanvasAnnotations.js
@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
+import Chip from '@material-ui/core/Chip';
 import List from '@material-ui/core/List';
 import ListItem from '@material-ui/core/ListItem';
 import ListItemText from '@material-ui/core/ListItemText';
@@ -89,6 +90,13 @@ export class CanvasAnnotations extends Component {
                     ruleSet={htmlSanitizationRuleSet}
                     htmlString={annotation.content}
                   />
+                  <div>
+                    {
+                      annotation.tags.map(tag => (
+                        <Chip size="small" variant="outlined" label={tag} id={tag} className={classes.chip} />
+                      ))
+                    }
+                  </div>
                 </ListItemText>
               </ListItem>
             ))
diff --git a/src/config/settings.js b/src/config/settings.js
index 66890bebb..b423cebd5 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -216,7 +216,7 @@ export default {
   },
   annotations: {
     htmlSanitizationRuleSet: 'iiif', // See src/lib/htmlRules.js for acceptable values
-    filteredMotivations: ['oa:commenting', 'sc:painting', 'commenting'],
+    filteredMotivations: ['oa:commenting', 'oa:tagging', 'sc:painting', 'commenting', 'tagging'],
   },
   classPrefix: 'mirador',
   displayAllAnnotations: false, // Configure if annotations to be displayed on the canvas by default when fetched
diff --git a/src/containers/CanvasAnnotations.js b/src/containers/CanvasAnnotations.js
index 67d5a803f..cab58bd7f 100644
--- a/src/containers/CanvasAnnotations.js
+++ b/src/containers/CanvasAnnotations.js
@@ -20,6 +20,7 @@ function getIdAndContentOfResources(resources) {
   return resources.map((resource, i) => ({
     content: resource.chars,
     id: resource.id,
+    tags: resource.tags,
     targetId: resource.targetId,
   }));
 }
@@ -60,6 +61,11 @@ const styles = theme => ({
     borderBottom: `0.5px solid ${theme.palette.divider}`,
     cursor: 'pointer',
   },
+  chip: {
+    backgroundColor: theme.palette.background.paper,
+    marginRight: theme.spacing(0.5),
+    marginTop: theme.spacing(1),
+  },
   sectionHeading: {
     paddingLeft: theme.spacing(2),
     paddingRight: theme.spacing(1),
diff --git a/src/lib/AnnotationItem.js b/src/lib/AnnotationItem.js
index 10d59223a..02f69f13e 100644
--- a/src/lib/AnnotationItem.js
+++ b/src/lib/AnnotationItem.js
@@ -11,6 +11,11 @@ export default class AnnotationItem {
     this.resource = resource;
   }
 
+  /** */
+  isOnlyTag() {
+    return (this.motivations.length === 1 && this.motivations[0] === 'tagging');
+  }
+
   /** */
   get id() {
     this._id = this._id || this.resource.id || uuid(); // eslint-disable-line no-underscore-dangle
@@ -47,6 +52,14 @@ export default class AnnotationItem {
     return this.body;
   }
 
+  /** */
+  get tags() {
+    if (this.isOnlyTag()) {
+      return this.body.map(r => r.value);
+    }
+    return this.body.filter(r => r.purpose === 'tagging').map(r => r.value);
+  }
+
   /** */
   get target() {
     return flatten(compact(new Array(this.resource.target)));
@@ -54,7 +67,8 @@ export default class AnnotationItem {
 
   /** */
   get chars() {
-    return this.body.map(r => r.value).join(' ');
+    if (this.isOnlyTag()) return null;
+    return this.body.filter(r => r.purpose !== 'tagging').map(r => r.value).join(' ');
   }
 
   /** */
diff --git a/src/lib/AnnotationResource.js b/src/lib/AnnotationResource.js
index 1ec5bf024..2a9437521 100644
--- a/src/lib/AnnotationResource.js
+++ b/src/lib/AnnotationResource.js
@@ -9,6 +9,11 @@ export default class AnnotationResource {
     this.resource = resource;
   }
 
+  /** */
+  isOnlyTag() {
+    return (this.motivations.length === 1 && this.motivations[0] === 'oa:tagging');
+  }
+
   /** */
   get id() {
     this._id = this._id || this.resource['@id'] || uuid(); // eslint-disable-line no-underscore-dangle
@@ -45,9 +50,17 @@ export default class AnnotationResource {
     return flatten(compact(new Array(this.resource.on)));
   }
 
+  /** */
+  get tags() {
+    if (this.isOnlyTag()) {
+      return this.resources.map(r => r.value);
+    }
+    return this.resources.filter(r => r['@type'] === 'oa:Tag').map(r => r.value);
+  }
+
   /** */
   get chars() {
-    return this.resources.map(r => r.chars).join(' ');
+    return this.resources.filter(r => r['@type'] !== 'oa:Tag').map(r => r.chars).join(' ');
   }
 
   /** */
-- 
GitLab