Repository: ProjectMirador/mirador (js)

Issue:
Replace JSON.parse(JSON.stringify(json)) with structuredClone
It should be faster and safer, and it has been supported in all major browsers since 2022.

Pull request description:
Replace JSON.parse(JSON.stringify(json)) with structuredClone
- see also https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone
- fixes #4082

Patch:
diff --git a/src/components/ThumbnailCanvasGrouping.js b/src/components/ThumbnailCanvasGrouping.js
index 6f39f8f3..f01d3992 100644
--- a/src/components/ThumbnailCanvasGrouping.js
+++ b/src/components/ThumbnailCanvasGrouping.js
@@ -39,7 +39,7 @@ export class ThumbnailCanvasGrouping extends PureComponent {
   /** */
   render() {
     const {
-      index, style, data, currentCanvasId, showThumbnailLabels,
+      index, style, data, currentCanvasId,
     } = this.props;
     const {
       canvasGroupings, position, height,
@@ -87,7 +87,7 @@ export class ThumbnailCanvasGrouping extends PureComponent {
             <IIIFThumbnail
               key={canvas.id}
               resource={canvas}
-              labelled={showThumbnailLabels}
+              labelled
               maxHeight={(position === 'far-right') ? style.height - (1.5 * SPACING) : height - (1.5 * SPACING)}
               variant="inside"
             />
@@ -103,6 +103,5 @@ ThumbnailCanvasGrouping.propTypes = {
   data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
   index: PropTypes.number.isRequired,
   setCanvas: PropTypes.func.isRequired,
-  showThumbnailLabels: PropTypes.bool.isRequired,
   style: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
 };
diff --git a/src/components/WorkspaceAdd.js b/src/components/WorkspaceAdd.js
index 58e26ca7..18e07fe1 100644
--- a/src/components/WorkspaceAdd.js
+++ b/src/components/WorkspaceAdd.js
@@ -1,4 +1,4 @@
-import { useRef, useState, useEffect } from 'react';
+import { useRef, useState } from 'react';
 import PropTypes from 'prop-types';
 import { styled } from '@mui/material/styles';
 import classNames from 'classnames';
@@ -44,14 +44,6 @@ export function WorkspaceAdd({
   const { t } = useTranslation();
   const [addResourcesOpen, setAddResourcesVisibility] = useState(false);
   const ref = useRef();
-  const [refWidth, setRefWidth] = useState('100%');
-
-  /** */
-  const updateRefWidth = () => {
-    if (ref.current) {
-      setRefWidth(ref.current.offsetWidth);
-    }
-  };
 
   /** */
   const handleDrop = ({ manifestId, manifestJson }, props, monitor) => {
@@ -92,19 +84,9 @@ export function WorkspaceAdd({
     addResource, catalog, setWorkspaceAddVisibility, t, ...rest,
   };
 
-  useEffect(() => {
-    window.addEventListener('resize', updateRefWidth);
-
-    updateRefWidth();
-
-    return () => {
-      window.removeEventListener('resize', updateRefWidth);
-    };
-  }, []);
-
   return (
     <IIIFDropTarget onDrop={handleDrop}>
-      <StyledWorkspaceAdd className={classNames(ns('workspace-add'))} ref={ref}>
+      <StyledWorkspaceAdd ref={ref} className={classNames(ns('workspace-add'))}>
         {catalog.length < 1 ? (
           <Grid
             alignItems="center"
@@ -156,8 +138,10 @@ export function WorkspaceAdd({
           sx={theme => ({
             '.MuiDrawer-paper': {
               borderTop: '0',
-              left: 'unset',
-              width: refWidth,
+              left: '0',
+              [theme.breakpoints.up('sm')]: {
+                left: '65px',
+              },
             },
             ...(!addResourcesOpen && {
               display: 'none',
diff --git a/src/config/settings.js b/src/config/settings.js
index c40dac40..cf8612fd 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -515,16 +515,15 @@ export default {
     defaultPosition: 'off', // Which position for the thumbnail navigation to be be displayed. Other possible values are "far-bottom" or "far-right"
     displaySettings: true, // Display the settings for this in WindowTopMenu
     height: 130, // height of entire ThumbnailNavigation area when position is "far-bottom"
-    showThumbnailLabels: true, // Configure if thumbnail labels should be displayed
     width: 100, // width of one canvas (doubled for book view) in ThumbnailNavigation area when position is "far-right"
   },
   workspace: {
-    allowNewWindows: true,
     draggingEnabled: true,
-    exposeModeOn: false, // unused?
-    height: 5000, // height of the elastic mode's virtual canvas
+    allowNewWindows: true,
     id: uuid(),
     isWorkspaceAddVisible: false, // Catalog/Workspace add window feature visible by default
+    exposeModeOn: false, // unused?
+    height: 5000, // height of the elastic mode's virtual canvas
     showZoomControls: true, // Configure if zoom controls should be displayed by default
     type: 'mosaic', // Which workspace type to load by default. Other possible values are "elastic". If "mosaic" or "elastic" are not selected no worksapce type will be used.
     viewportPosition: { // center coordinates for the elastic mode workspace
diff --git a/src/containers/ThumbnailCanvasGrouping.js b/src/containers/ThumbnailCanvasGrouping.js
index 61ed97c8..a8e7c595 100644
--- a/src/containers/ThumbnailCanvasGrouping.js
+++ b/src/containers/ThumbnailCanvasGrouping.js
@@ -2,7 +2,7 @@ import { compose } from 'redux';
 import { connect } from 'react-redux';
 import { withPlugins } from '../extend/withPlugins';
 import * as actions from '../state/actions';
-import { getCurrentCanvas, getConfig } from '../state/selectors';
+import { getCurrentCanvas } from '../state/selectors';
 import { ThumbnailCanvasGrouping } from '../components/ThumbnailCanvasGrouping';
 
 /**
@@ -21,7 +21,6 @@ const mapDispatchToProps = (dispatch, { data }) => ({
  */
 const mapStateToProps = (state, { data }) => ({
   currentCanvasId: (getCurrentCanvas(state, { windowId: data.windowId }) || {}).id,
-  showThumbnailLabels: getConfig(state).thumbnailNavigation.showThumbnailLabels,
 });
 
 const enhance = compose(
diff --git a/src/state/selectors/manifests.js b/src/state/selectors/manifests.js
index 04e3661e..ec7e93ae 100644
--- a/src/state/selectors/manifests.js
+++ b/src/state/selectors/manifests.js
@@ -9,8 +9,8 @@ import { getConfig } from './config';
 /** */
 function createManifestoInstance(json, locale) {
   if (!json) return undefined;
-  // Use JSON stringify/parse to create a deep copy and prevent Manifesto from mutating the json
-  const manifestoObject = Utils.parseManifest(JSON.parse(JSON.stringify(json)), locale ? { locale } : undefined);
+  // Use structuredClone to create a deep copy and prevent Manifesto from mutating the json
+  const manifestoObject = Utils.parseManifest(structuredClone(json), locale ? { locale } : undefined);
   // Local patching of Manifesto so that when its a Collection, it behaves similarly
   if (typeof manifestoObject.getSequences != 'function') {
     manifestoObject.getSequences = () => [];


Test patch:
diff --git a/__tests__/fixtures/config/export.example.json b/__tests__/fixtures/config/export.example.json
index edabdb28..1075e6e3 100644
--- a/__tests__/fixtures/config/export.example.json
+++ b/__tests__/fixtures/config/export.example.json
@@ -179,8 +179,7 @@
     "thumbnailNavigation": {
       "defaultPosition": "far-bottom",
       "height": 150,
-      "width": 100,
-      "showThumbnailLabels": true
+      "width": 100
     },
     "workspace": {
       "type": "mosaic"
diff --git a/__tests__/src/components/ThumbnailCanvasGrouping.test.js b/__tests__/src/components/ThumbnailCanvasGrouping.test.js
index 3f1effd2..e84ce267 100644
--- a/__tests__/src/components/ThumbnailCanvasGrouping.test.js
+++ b/__tests__/src/components/ThumbnailCanvasGrouping.test.js
@@ -18,7 +18,6 @@ function createWrapper(props) {
         top: 0,
         width: 100,
       }}
-      showThumbnailLabels
       {...props}
     />,
   );
diff --git a/__tests__/src/selectors/manifests.test.js b/__tests__/src/selectors/manifests.test.js
index 3c10199d..68122efb 100644
--- a/__tests__/src/selectors/manifests.test.js
+++ b/__tests__/src/selectors/manifests.test.js
@@ -506,7 +506,7 @@ describe('getManifestSearchService', () => {
   });
 
   it('supports v1 of the search spec', () => {
-    const v1 = JSON.parse(JSON.stringify(manifestFixtureFg165hz3589));
+    const v1 = structuredClone(manifestFixtureFg165hz3589);
     v1.service[0].profile = 'http://iiif.io/api/search/1/search';
     const state = { manifests: { x: { json: v1 } } };
     expect(getManifestSearchService(state, { manifestId: 'x' }).id).toEqual('https://contentsearch.stanford.edu/fg165hz3589/search');
@@ -525,7 +525,7 @@ describe('getManifestAutocompleteService', () => {
   });
 
   it('supports v1 of the search spec', () => {
-    const v1 = JSON.parse(JSON.stringify(manifestFixtureFg165hz3589));
+    const v1 = structuredClone(manifestFixtureFg165hz3589);
     v1.service[0].profile = 'http://iiif.io/api/search/1/search';
     v1.service[0].service.profile = 'http://iiif.io/api/search/1/autocomplete';
     const state = { manifests: { x: { json: v1 } } };