From 8dae6b6e64ccc5e85dc6e72e94ebcf2b9d6e9b1e Mon Sep 17 00:00:00 2001
From: Lutz Helm <helm@ub.uni-leipzig.de>
Date: Wed, 16 Feb 2022 09:59:22 +0100
Subject: [PATCH] Fix #3522, add selector and UI for related

---
 __tests__/fixtures/version-2/related.json     | 16 ++++++
 .../components/ManifestRelatedLinks.test.js   | 46 +++++++++++++++--
 __tests__/src/selectors/manifests.test.js     | 49 +++++++++++++++++++
 src/components/ManifestRelatedLinks.js        | 36 +++++++++++---
 src/containers/ManifestRelatedLinks.js        |  6 ++-
 src/locales/de/translation.json               |  1 +
 src/locales/en/translation.json               |  1 +
 src/state/selectors/manifests.js              | 45 ++++++++++++++++-
 8 files changed, 186 insertions(+), 14 deletions(-)
 create mode 100644 __tests__/fixtures/version-2/related.json

diff --git a/__tests__/fixtures/version-2/related.json b/__tests__/fixtures/version-2/related.json
new file mode 100644
index 000000000..f87bf9663
--- /dev/null
+++ b/__tests__/fixtures/version-2/related.json
@@ -0,0 +1,16 @@
+{
+  "@context": "http://iiif.io/api/presentation/2/context.json",
+  "@id": "http://example.com/iiif/manifest/related-urls.json",
+  "@type": "sc:Manifest",
+  "related": [
+    "http://example.com/related1",
+    {
+      "@id": "http://example.com/related2"
+    },
+    {
+      "@id": "http://example.com/related3",
+      "format": "text/html",
+      "label": "Something related"
+    }
+  ]
+}
diff --git a/__tests__/src/components/ManifestRelatedLinks.test.js b/__tests__/src/components/ManifestRelatedLinks.test.js
index 234b5bab9..4bf7994f7 100644
--- a/__tests__/src/components/ManifestRelatedLinks.test.js
+++ b/__tests__/src/components/ManifestRelatedLinks.test.js
@@ -21,6 +21,16 @@ describe('ManifestRelatedLinks', () => {
             },
           ]}
           manifestUrl="http://example.com/"
+          related={[
+            {
+              value: 'http://example.com/related',
+            },
+            {
+              format: 'video/ogg',
+              label: 'Video',
+              value: 'http://example.com/video',
+            },
+          ]}
           renderings={[
             {
               label: 'PDF Version',
@@ -81,16 +91,42 @@ describe('ManifestRelatedLinks', () => {
       ).toBe(true);
     });
 
-    it('renders manifest seeAlso information', () => {
+    it('renders related information', () => {
       expect(
         wrapper.find(Typography).at(5)
           .matchesElement(
-            <Typography component="dt">iiif_seeAlso</Typography>,
+            <Typography component="dt">iiif_related</Typography>,
           ),
       ).toBe(true);
 
       expect(
         wrapper.find(Typography).at(6)
+          .matchesElement(
+            <Typography component="dd"><Link href="http://example.com/related">http://example.com/related</Link></Typography>,
+          ),
+      ).toBe(true);
+
+      expect(
+        wrapper.find(Typography).at(7)
+          .matchesElement(
+            <Typography component="dd">
+              <Link href="http://example.com/video">Video</Link>
+              <Typography>(video/ogg)</Typography>
+            </Typography>,
+          ),
+      ).toBe(true);
+    });
+
+    it('renders manifest seeAlso information', () => {
+      expect(
+        wrapper.find(Typography).at(9)
+          .matchesElement(
+            <Typography component="dt">iiif_seeAlso</Typography>,
+          ),
+      ).toBe(true);
+
+      expect(
+        wrapper.find(Typography).at(10)
           .matchesElement(
             <Typography component="dd">
               <Link href="http://example.com/a">A</Link>
@@ -100,7 +136,7 @@ describe('ManifestRelatedLinks', () => {
       ).toBe(true);
 
       expect(
-        wrapper.find(Typography).at(8)
+        wrapper.find(Typography).at(12)
           .matchesElement(
             <Typography component="dd"><Link href="http://example.com/b">http://example.com/b</Link></Typography>,
           ),
@@ -109,14 +145,14 @@ describe('ManifestRelatedLinks', () => {
 
     it('renders manifest links', () => {
       expect(
-        wrapper.find(Typography).at(9)
+        wrapper.find(Typography).at(13)
           .matchesElement(
             <Typography component="dt">iiif_manifest</Typography>,
           ),
       ).toBe(true);
 
       expect(
-        wrapper.find(Typography).at(10)
+        wrapper.find(Typography).at(14)
           .matchesElement(
             <Typography component="dd"><Link href="http://example.com/">http://example.com/</Link></Typography>,
           ),
diff --git a/__tests__/src/selectors/manifests.test.js b/__tests__/src/selectors/manifests.test.js
index 3c174506f..28b0461ad 100644
--- a/__tests__/src/selectors/manifests.test.js
+++ b/__tests__/src/selectors/manifests.test.js
@@ -6,6 +6,7 @@ import manifestFixtureSn904cj3429 from '../../fixtures/version-2/sn904cj3429.jso
 import manifestFixturev3001 from '../../fixtures/version-3/001.json';
 import manifestFixtureWithAProvider from '../../fixtures/version-3/with_a_provider.json';
 import manifestFixtureFg165hz3589 from '../../fixtures/version-2/fg165hz3589.json';
+import manifestFixtureRelated from '../../fixtures/version-2/related.json';
 import {
   getManifestoInstance,
   getManifestLocale,
@@ -18,8 +19,10 @@ import {
   getManifestTitle,
   getManifestThumbnail,
   getManifestMetadata,
+  getManifestRelated,
   getManifestRelatedContent,
   getManifestRenderings,
+  getManifestSeeAlso,
   getManifestUrl,
   getMetadataLocales,
   getRequiredStatement,
@@ -210,6 +213,33 @@ describe('getManifestRenderings', () => {
   });
 });
 
+describe('getManifestRelated', () => {
+  it('should return manifest related', () => {
+    const state = { manifests: { x: { json: manifestFixtureRelated } } };
+    const received = getManifestRelated(state, { manifestId: 'x' });
+    expect(received).toEqual([
+      {
+        value: 'http://example.com/related1',
+      },
+      {
+        format: undefined,
+        label: null,
+        value: 'http://example.com/related2',
+      },
+      {
+        format: 'text/html',
+        label: 'Something related',
+        value: 'http://example.com/related3',
+      },
+    ]);
+  });
+
+  it('should return undefined if manifest undefined', () => {
+    const received = getManifestRelated({ manifests: {} }, { manifestId: 'x' });
+    expect(received).toBeUndefined();
+  });
+});
+
 describe('getManifestRelatedContent', () => {
   it('should return manifest seeAlso content', () => {
     const state = { manifests: { x: { json: manifestFixtureSn904cj3429 } } };
@@ -229,6 +259,25 @@ describe('getManifestRelatedContent', () => {
   });
 });
 
+describe('getManifestSeeAlso', () => {
+  it('should return manifest seeAlso content', () => {
+    const state = { manifests: { x: { json: manifestFixtureSn904cj3429 } } };
+    const received = getManifestSeeAlso(state, { manifestId: 'x' });
+    expect(received).toEqual([
+      {
+        format: 'application/mods+xml',
+        label: null,
+        value: 'https://purl.stanford.edu/sn904cj3429.mods',
+      },
+    ]);
+  });
+
+  it('should return undefined if manifest undefined', () => {
+    const received = getManifestSeeAlso({ manifests: {} }, { manifestId: 'x' });
+    expect(received).toBeUndefined();
+  });
+});
+
 describe('getManifestUrl', () => {
   it('should return manifest url', () => {
     const state = { manifests: { x: { json: manifestFixtureWithAProvider } } };
diff --git a/src/components/ManifestRelatedLinks.js b/src/components/ManifestRelatedLinks.js
index 533b4522d..943d287ff 100644
--- a/src/components/ManifestRelatedLinks.js
+++ b/src/components/ManifestRelatedLinks.js
@@ -20,6 +20,7 @@ export class ManifestRelatedLinks extends Component {
       classes,
       homepage,
       manifestUrl,
+      related,
       renderings,
       seeAlso,
       id,
@@ -68,17 +69,34 @@ export class ManifestRelatedLinks extends Component {
               }
             </>
           )}
+          { related && (
+            <>
+              <Typography variant="subtitle2" component="dt">{t('iiif_related')}</Typography>
+              {
+                related.map(relatedItem => (
+                  <Typography key={relatedItem.value} variant="body1" component="dd">
+                    <Link target="_blank" rel="noopener noreferrer" href={relatedItem.value}>
+                      {relatedItem.label || relatedItem.value}
+                    </Link>
+                    { relatedItem.format && (
+                      <Typography component="span">{` (${relatedItem.format})`}</Typography>
+                    )}
+                  </Typography>
+                ))
+              }
+            </>
+          )}
           { seeAlso && (
             <>
               <Typography variant="subtitle2" component="dt">{t('iiif_seeAlso')}</Typography>
               {
-                seeAlso.map(related => (
-                  <Typography key={related.value} variant="body1" component="dd">
-                    <Link target="_blank" rel="noopener noreferrer" href={related.value}>
-                      {related.label || related.value}
+                seeAlso.map(seeAlsoItem => (
+                  <Typography key={seeAlsoItem.value} variant="body1" component="dd">
+                    <Link target="_blank" rel="noopener noreferrer" href={seeAlsoItem.value}>
+                      {seeAlsoItem.label || seeAlsoItem.value}
                     </Link>
-                    { related.format && (
-                      <Typography component="span">{` (${related.format})`}</Typography>
+                    { seeAlsoItem.format && (
+                      <Typography component="span">{` (${seeAlsoItem.format})`}</Typography>
                     )}
                   </Typography>
                 ))
@@ -110,6 +128,11 @@ ManifestRelatedLinks.propTypes = {
   })),
   id: PropTypes.string.isRequired,
   manifestUrl: PropTypes.string,
+  related: PropTypes.arrayOf(PropTypes.shape({
+    format: PropTypes.string,
+    label: PropTypes.string,
+    value: PropTypes.string,
+  })),
   renderings: PropTypes.arrayOf(PropTypes.shape({
     label: PropTypes.string,
     value: PropTypes.string,
@@ -125,6 +148,7 @@ ManifestRelatedLinks.propTypes = {
 ManifestRelatedLinks.defaultProps = {
   homepage: null,
   manifestUrl: null,
+  related: null,
   renderings: null,
   seeAlso: null,
   t: key => key,
diff --git a/src/containers/ManifestRelatedLinks.js b/src/containers/ManifestRelatedLinks.js
index 005029f22..6bcc35677 100644
--- a/src/containers/ManifestRelatedLinks.js
+++ b/src/containers/ManifestRelatedLinks.js
@@ -5,8 +5,9 @@ import { withStyles } from '@material-ui/core/styles';
 import { withPlugins } from '../extend/withPlugins';
 import {
   getManifestHomepage,
-  getManifestRelatedContent,
+  getManifestRelated,
   getManifestRenderings,
+  getManifestSeeAlso,
   getManifestUrl,
 } from '../state/selectors';
 import { ManifestRelatedLinks } from '../components/ManifestRelatedLinks';
@@ -19,8 +20,9 @@ import { ManifestRelatedLinks } from '../components/ManifestRelatedLinks';
 const mapStateToProps = (state, { id, windowId }) => ({
   homepage: getManifestHomepage(state, { windowId }),
   manifestUrl: getManifestUrl(state, { windowId }),
+  related: getManifestRelated(state, { windowId }),
   renderings: getManifestRenderings(state, { windowId }),
-  seeAlso: getManifestRelatedContent(state, { windowId }),
+  seeAlso: getManifestSeeAlso(state, { windowId }),
 });
 
 const styles = {
diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json
index b1d3ca8b7..c27976c6b 100644
--- a/src/locales/de/translation.json
+++ b/src/locales/de/translation.json
@@ -57,6 +57,7 @@
     "hideZoomControls": "Zoomsteuerung verbergen",
     "iiif_homepage": "Über diese Ressource",
     "iiif_manifest": "IIIF-Manifest",
+    "iiif_related": "Verwandtes",
     "iiif_renderings": "Alternative Formate",
     "iiif_seeAlso": "Siehe auch",
     "import" : "Importieren",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 1fe15e741..a7f8b6993 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -59,6 +59,7 @@
     "hideZoomControls": "Hide zoom controls",
     "iiif_homepage": "About this resource",
     "iiif_manifest": "IIIF manifest",
+    "iiif_related": "Related",
     "iiif_renderings": "Alternate formats",
     "iiif_seeAlso": "See also",
     "import" : "Import",
diff --git a/src/state/selectors/manifests.js b/src/state/selectors/manifests.js
index 44abe0435..48f25eefc 100644
--- a/src/state/selectors/manifests.js
+++ b/src/state/selectors/manifests.js
@@ -153,13 +153,14 @@ export const getManifestRenderings = createSelector(
 
 /**
 * Return the IIIF v2/v3 seeAlso data from a manifest or null
+*
 * @param {object} state
 * @param {object} props
 * @param {string} props.manifestId
 * @param {string} props.windowId
 * @return {String|null}
 */
-export const getManifestRelatedContent = createSelector(
+export const getManifestSeeAlso = createSelector(
   [
     getProperty('seeAlso'),
     getManifestLocale,
@@ -175,6 +176,48 @@ export const getManifestRelatedContent = createSelector(
     )),
 );
 
+/**
+* Return the IIIF v2/v3 seeAlso data from a manifest or null
+*
+* @param {object} state
+* @param {object} props
+* @param {string} props.manifestId
+* @param {string} props.windowId
+* @return {String|null}
+* @deprecated This does not actually return the content of "related" and
+* might be removed in a future version.
+* @see getManifestSeeAlso
+*/
+export const getManifestRelatedContent = getManifestSeeAlso;
+
+/**
+* Return the IIIF v2 realated links manifest or null
+* @param {object} state
+* @param {object} props
+* @param {string} props.manifestId
+* @param {string} props.windowId
+* @return {String|null}
+*/
+export const getManifestRelated = createSelector(
+  [
+    getProperty('related'),
+    getManifestLocale,
+  ],
+  (relatedLinks, locale) => relatedLinks
+    && asArray(relatedLinks).map(related => (
+      typeof related === 'string'
+        ? {
+          value: related,
+        }
+        : {
+          format: related.format,
+          label: PropertyValue.parse(related.label, locale)
+            .getValue(),
+          value: related.id || related['@id'],
+        }
+    )),
+);
+
 /**
 * Return the IIIF requiredStatement (v3) or attribution (v2) data from a manifest or null
 * @param {object} state
-- 
GitLab