diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33f067109..f851ae325 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
+## [11.12.2](https://github.com/RedTurtle/design-comuni-plone-theme/compare/v11.12.1...v11.12.2) (2024-05-27)
+
+
+### Maintenance
+
+* added more logging for some errors ([73a6d13](https://github.com/RedTurtle/design-comuni-plone-theme/commit/73a6d13969775a0d2e52a8794211154a6f8c4da0))
+* updated repository info in package.json ([ac3ae90](https://github.com/RedTurtle/design-comuni-plone-theme/commit/ac3ae90b55b8e293bf7dc46394c4a5b81c8db035))
+
+
+### Documentation
+
+* updated publiccode ([2a28968](https://github.com/RedTurtle/design-comuni-plone-theme/commit/2a28968d62a3800df080842dfc399f7433050b75))
+
## [11.12.1](https://github.com/redturtle/design-comuni-plone-theme/compare/v11.12.0...v11.12.1) (2024-05-21)
diff --git a/package.json b/package.json
index 347657466..9f55a49dc 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "design-comuni-plone-theme",
"description": "Volto Theme for Italia design guidelines",
"license": "GPL-v3",
- "version": "11.12.1",
+ "version": "11.12.2",
"main": "src/index.js",
"repository": {
"type": "git",
diff --git a/publiccode.yml b/publiccode.yml
index dc1181b79..251a49938 100644
--- a/publiccode.yml
+++ b/publiccode.yml
@@ -227,9 +227,9 @@ maintenance:
name: io-Comune - Il sito AgID per Comuni ed Enti Pubblici
platforms:
- web
-releaseDate: '2024-05-21'
+releaseDate: '2024-05-27'
softwareType: standalone/web
-softwareVersion: 11.12.1
+softwareVersion: 11.12.2
url: 'https://github.com/italia/design-comuni-plone-theme'
usedBy:
- ASP Comuni Modenesi Area Nord
diff --git a/src/customizations/volto/components/theme/Error/Error.jsx b/src/customizations/volto/components/theme/Error/Error.jsx
new file mode 100644
index 000000000..4ba225652
--- /dev/null
+++ b/src/customizations/volto/components/theme/Error/Error.jsx
@@ -0,0 +1,76 @@
+/**
+ * @module components/theme/Error/Error
+ * Customization:
+ * - added logging of errors
+ */
+
+import React from 'react';
+import loadable from '@loadable/component';
+import config from '@plone/volto/registry';
+
+const sentryLibraries = {
+ Sentry: loadable.lib(
+ () => import(/* webpackChunkName: "s_entry-browser" */ '@sentry/browser'),
+ ),
+};
+
+/**
+ * Error function.
+ * @function Error
+ * @returns {string} Markup of the error page.
+ */
+const Error = (props) => {
+ const { views } = config;
+ const { error } = props;
+ let FoundView;
+
+ // CUSTOMIZATION: added logging of errors
+ const notifySentry = (error) => {
+ const loaders = Object.entries(sentryLibraries).map(
+ ([name, Lib]) =>
+ new Promise((resolve) =>
+ Lib.load().then((mod) => resolve([name, mod])),
+ ),
+ );
+ Promise.all(loaders).then((libs) => {
+ const libraries = Object.assign(
+ {},
+ ...libs.map(([name, lib]) => ({ [name]: lib })),
+ );
+ libraries.Sentry.captureException(error);
+ });
+ };
+
+ if (error.status === undefined) {
+ // For some reason, while development and if CORS is in place and the
+ // requested resource is 404, it returns undefined as status, then the
+ // next statement will fail
+ // eslint-disable-next-line no-console
+ console.error(
+ 'DEV MODE CORS ERROR in Error component: ',
+ JSON.stringify(props, null, 2),
+ );
+ notifySentry(props);
+ FoundView = views.errorViews.corsError;
+ } else {
+ if (error.status.toString() === 'corsError') {
+ // eslint-disable-next-line no-console
+ console.error(
+ 'CORS ERROR in Error component: ',
+ JSON.stringify(props, null, 2),
+ );
+ notifySentry(props);
+ }
+ FoundView = views.errorViews[error.status.toString()];
+ }
+ if (!FoundView) {
+ FoundView = views.errorViews['404']; // default to 404
+ }
+ return (
+
+
+
+ );
+};
+
+export default Error;
diff --git a/src/customizations/volto/components/theme/View/View.jsx b/src/customizations/volto/components/theme/View/View.jsx
new file mode 100644
index 000000000..2714ed81e
--- /dev/null
+++ b/src/customizations/volto/components/theme/View/View.jsx
@@ -0,0 +1,340 @@
+/**
+ * View container.
+ * @module components/theme/View/View
+ * Customization:
+ * - added logging of errors
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { compose } from 'redux';
+import { Redirect } from 'react-router-dom';
+import { Portal } from 'react-portal';
+import { injectIntl } from 'react-intl';
+import qs from 'query-string';
+
+import loadable from '@loadable/component';
+
+import {
+ ContentMetadataTags,
+ Comments,
+ Tags,
+ Toolbar,
+} from '@plone/volto/components';
+import { listActions, getContent } from '@plone/volto/actions';
+import {
+ BodyClass,
+ getBaseUrl,
+ flattenToAppURL,
+ getLayoutFieldname,
+ hasApiExpander,
+} from '@plone/volto/helpers';
+
+import config from '@plone/volto/registry';
+
+const sentryLibraries = {
+ Sentry: loadable.lib(
+ () => import(/* webpackChunkName: "s_entry-browser" */ '@sentry/browser'),
+ ),
+};
+
+/**
+ * View container class.
+ * @class View
+ * @extends Component
+ */
+class View extends Component {
+ /**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+ static propTypes = {
+ actions: PropTypes.shape({
+ object: PropTypes.arrayOf(PropTypes.object),
+ object_buttons: PropTypes.arrayOf(PropTypes.object),
+ user: PropTypes.arrayOf(PropTypes.object),
+ }),
+ listActions: PropTypes.func.isRequired,
+ /**
+ * Action to get the content
+ */
+ getContent: PropTypes.func.isRequired,
+ /**
+ * Pathname of the object
+ */
+ pathname: PropTypes.string.isRequired,
+ location: PropTypes.shape({
+ search: PropTypes.string,
+ pathname: PropTypes.string,
+ }).isRequired,
+ /**
+ * Version id of the object
+ */
+ versionId: PropTypes.string,
+ /**
+ * Content of the object
+ */
+ content: PropTypes.shape({
+ /**
+ * Layout of the object
+ */
+ layout: PropTypes.string,
+ /**
+ * Allow discussion of the object
+ */
+ allow_discussion: PropTypes.bool,
+ /**
+ * Title of the object
+ */
+ title: PropTypes.string,
+ /**
+ * Description of the object
+ */
+ description: PropTypes.string,
+ /**
+ * Type of the object
+ */
+ '@type': PropTypes.string,
+ /**
+ * Subjects of the object
+ */
+ subjects: PropTypes.arrayOf(PropTypes.string),
+ is_folderish: PropTypes.bool,
+ }),
+ error: PropTypes.shape({
+ /**
+ * Error type
+ */
+ status: PropTypes.number,
+ }),
+ };
+
+ /**
+ * Default properties.
+ * @property {Object} defaultProps Default properties.
+ * @static
+ */
+ static defaultProps = {
+ actions: null,
+ content: null,
+ versionId: null,
+ error: null,
+ };
+
+ state = {
+ hasObjectButtons: null,
+ isClient: false,
+ };
+
+ componentDidMount() {
+ // Do not trigger the actions action if the expander is present
+ if (!hasApiExpander('actions', getBaseUrl(this.props.pathname))) {
+ this.props.listActions(getBaseUrl(this.props.pathname));
+ }
+
+ this.props.getContent(
+ getBaseUrl(this.props.pathname),
+ this.props.versionId,
+ );
+ this.setState({ isClient: true });
+ }
+
+ /**
+ * Component will receive props
+ * @method componentWillReceiveProps
+ * @param {Object} nextProps Next properties
+ * @returns {undefined}
+ */
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (nextProps.pathname !== this.props.pathname) {
+ // Do not trigger the actions action if the expander is present
+ if (!hasApiExpander('actions', getBaseUrl(nextProps.pathname))) {
+ this.props.listActions(getBaseUrl(nextProps.pathname));
+ }
+
+ this.props.getContent(
+ getBaseUrl(nextProps.pathname),
+ this.props.versionId,
+ );
+ }
+
+ if (nextProps.actions.object_buttons) {
+ const objectButtons = nextProps.actions.object_buttons;
+ this.setState({
+ hasObjectButtons: !!objectButtons.length,
+ });
+ }
+ }
+
+ /**
+ * Default fallback view
+ * @method getViewDefault
+ * @returns {string} Markup for component.
+ */
+ getViewDefault = () => config.views.defaultView;
+
+ /**
+ * Get view by content type
+ * @method getViewByType
+ * @returns {string} Markup for component.
+ */
+ getViewByType = () =>
+ config.views.contentTypesViews[this.props.content['@type']] || null;
+
+ /**
+ * Get view by content layout property
+ * @method getViewByLayout
+ * @returns {string} Markup for component.
+ */
+ getViewByLayout = () =>
+ config.views.layoutViews[
+ this.props.content[getLayoutFieldname(this.props.content)]
+ ] || null;
+
+ /**
+ * Cleans the component displayName (specially for connected components)
+ * which have the Connect(componentDisplayName)
+ * @method cleanViewName
+ * @param {string} dirtyDisplayName The displayName
+ * @returns {string} Clean displayName (no Connect(...)).
+ */
+ cleanViewName = (dirtyDisplayName) =>
+ dirtyDisplayName
+ .replace('Connect(', '')
+ .replace('injectIntl(', '')
+ .replace(')', '')
+ .replace('connect(', '')
+ .toLowerCase();
+
+ // CUSTOMIZATION: added logging of errors
+ notifySentry = (error) => {
+ const loaders = Object.entries(sentryLibraries).map(
+ ([name, Lib]) =>
+ new Promise((resolve) =>
+ Lib.load().then((mod) => resolve([name, mod])),
+ ),
+ );
+ Promise.all(loaders).then((libs) => {
+ const libraries = Object.assign(
+ {},
+ ...libs.map(([name, lib]) => ({ [name]: lib })),
+ );
+ libraries.Sentry.captureException(error);
+ });
+ };
+
+ /**
+ * Render method.
+ * @method render
+ * @returns {string} Markup for the component.
+ */
+ render() {
+ const { views } = config;
+ if (this.props.error && this.props.error.code === 301) {
+ const redirect = flattenToAppURL(this.props.error.url).split('?')[0];
+ return ;
+ } else if (this.props.error && !this.props.connectionRefused) {
+ let FoundView;
+ if (this.props.error.status === undefined) {
+ // For some reason, while development and if CORS is in place and the
+ // requested resource is 404, it returns undefined as status, then the
+ // next statement will fail
+ // eslint-disable-next-line no-console
+ console.error(
+ 'DEV MODE CORS ERROR in View component: ',
+ JSON.stringify(this.props, null, 2),
+ );
+ this.notifySentry(this.props);
+ FoundView = views.errorViews.corsError;
+ } else {
+ if (this.props.error.status.toString() === 'corsError') {
+ // eslint-disable-next-line no-console
+ console.error(
+ 'CORS ERROR in View component: ',
+ JSON.stringify(this.props, null, 2),
+ );
+ this.notifySentry(this.props);
+ }
+ FoundView = views.errorViews[this.props.error.status.toString()];
+ }
+ if (!FoundView) {
+ FoundView = views.errorViews['404']; // default to 404
+ }
+ return (
+
+
+
+ );
+ }
+ if (!this.props.content) {
+ return ;
+ }
+ const RenderedView =
+ this.getViewByLayout() || this.getViewByType() || this.getViewDefault();
+
+ return (
+
+
+ {/* Body class if displayName in component is set */}
+
+
+ {config.settings.showTags &&
+ this.props.content.subjects &&
+ this.props.content.subjects.length > 0 && (
+
+ )}
+ {/* Add opt-in social sharing if required, disabled by default */}
+ {/* In the future this might be parameterized from the app config */}
+ {/*
*/}
+ {this.props.content.allow_discussion && (
+
+ )}
+ {this.state.isClient && (
+
+ } />
+
+ )}
+
+ );
+ }
+}
+
+export default compose(
+ injectIntl,
+ connect(
+ (state, props) => ({
+ actions: state.actions.actions,
+ token: state.userSession.token,
+ content: state.content.data,
+ error: state.content.get.error,
+ apiError: state.apierror.error,
+ connectionRefused: state.apierror.connectionRefused,
+ pathname: props.location.pathname,
+ versionId:
+ qs.parse(props.location.search) &&
+ qs.parse(props.location.search).version,
+ }),
+ {
+ listActions,
+ getContent,
+ },
+ ),
+)(View);