diff --git a/CHANGELOG.md b/CHANGELOG.md
index d927dceb..f9271273 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
+### [1.33.0](https://github.com/eea/volto-eea-website-theme/compare/1.32.1...1.33.0) - 2 April 2024
+
+#### :rocket: New Features
+
+- feat(columnsBlock): move tocEntries definition to volto-columns-block [Miu Razvan - [`eb55ff8`](https://github.com/eea/volto-eea-website-theme/commit/eb55ff8753e443c83fd4a8bb3dca8c2b2a78a782)]
+
+#### :bug: Bug Fixes
+
+- fix(toc): use the correct function to render entries in toc [Miu Razvan - [`869ae7c`](https://github.com/eea/volto-eea-website-theme/commit/869ae7cbe2ec555ebfe563d66bb00e5bc80651c2)]
+
+#### :house: Internal changes
+
+- chore: Cleanup package.json [alin - [`c5a1c37`](https://github.com/eea/volto-eea-website-theme/commit/c5a1c370eec27a0ebea9df51dde12040580def2f)]
+
+#### :hammer_and_wrench: Others
+
+- Update index.js to fix jslint issue [ichim-david - [`89b9cef`](https://github.com/eea/volto-eea-website-theme/commit/89b9cefbe35f780bdd07fa6f44234fbfc2fe7bb0)]
+- Update package.json [ichim-david - [`a0ecadf`](https://github.com/eea/volto-eea-website-theme/commit/a0ecadf6efcdeb0fec4ee533d27a6c6787029373)]
+- Revert "fix(blocks): Allow image block urls to be external (#214)" [David Ichim - [`2bbc620`](https://github.com/eea/volto-eea-website-theme/commit/2bbc620d0a375d69ba7982c8e3113628365bb8da)]
### [1.32.1](https://github.com/eea/volto-eea-website-theme/compare/1.32.0...1.32.1) - 26 March 2024
#### :rocket: New Features
diff --git a/package.json b/package.json
index 78e17702..d1af8281 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@eeacms/volto-eea-website-theme",
- "version": "1.32.1",
+ "version": "1.33.0",
"description": "@eeacms/volto-eea-website-theme: Volto add-on",
"main": "src/index.js",
"author": "European Environment Agency: IDM2 A-Team",
@@ -79,4 +79,4 @@
"cypress:open": "make cypress-open",
"prepare": "husky install"
}
-}
\ No newline at end of file
+}
diff --git a/src/components/manage/Blocks/LayoutSettings/LayoutSettingsView.test.jsx b/src/components/manage/Blocks/LayoutSettings/LayoutSettingsView.test.jsx
new file mode 100644
index 00000000..a954a6a2
--- /dev/null
+++ b/src/components/manage/Blocks/LayoutSettings/LayoutSettingsView.test.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { Provider } from 'react-intl-redux';
+import configureStore from 'redux-mock-store';
+import { Router } from 'react-router-dom';
+import { createMemoryHistory } from 'history';
+import LayoutSettingsView from './LayoutSettingsView';
+
+const mockStore = configureStore();
+let history = createMemoryHistory();
+
+describe('LayoutSettingsView Component', () => {
+ it('renders without crashing', () => {
+ const store = mockStore({
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+ });
+
+ const data = {
+ '@layout': 'e28ec238-4cd7-4b72-8025-66da44a6062f',
+ '@type': 'layoutSettings',
+ block: '87911ec6-4242-4bae-b6a5-9b28151169fa',
+ body_class: 'body-class-1',
+ layout_size: 'container_view',
+ };
+
+ const { container } = render(
+
+
+
+
+ ,
+ );
+
+ expect(container).toBeTruthy();
+ });
+});
+
+describe('LayoutSettingsView Component', () => {
+ it('renders without crashing with multiple classes', () => {
+ const store = mockStore({
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+ });
+
+ const data = {
+ '@layout': 'e28ec238-4cd7-4b72-8025-66da44a6062f',
+ '@type': 'layoutSettings',
+ block: '87911ec6-4242-4bae-b6a5-9b28151169fa',
+ body_class: ['body-class-1', 'body-class-2'],
+ layout_size: 'container_view',
+ };
+
+ const { container } = render(
+
+
+
+
+ ,
+ );
+
+ expect(container).toBeTruthy();
+ });
+});
diff --git a/src/components/manage/Blocks/LayoutSettings/schema.js b/src/components/manage/Blocks/LayoutSettings/schema.js
index b5849804..6a15e57a 100644
--- a/src/components/manage/Blocks/LayoutSettings/schema.js
+++ b/src/components/manage/Blocks/LayoutSettings/schema.js
@@ -32,6 +32,7 @@ export const EditSchema = () => {
['homepage', 'Homepage'],
['homepage-inverse', 'Homepage inverse'],
],
+ widget: 'creatable_select',
},
},
};
diff --git a/src/components/theme/Widgets/CreatableSelectWidget.jsx b/src/components/theme/Widgets/CreatableSelectWidget.jsx
new file mode 100644
index 00000000..fcc6bfb0
--- /dev/null
+++ b/src/components/theme/Widgets/CreatableSelectWidget.jsx
@@ -0,0 +1,304 @@
+/**
+ * CreatableSelectWidget component.
+ * @module components/manage/Widgets/SelectWidget
+ *
+ * A copy of the SelectWidget component. The only difference is that is uses the Creatable component as a base
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { compose } from 'redux';
+import { map } from 'lodash';
+import { defineMessages, injectIntl } from 'react-intl';
+import {
+ getVocabFromHint,
+ getVocabFromField,
+ getVocabFromItems,
+} from '@plone/volto/helpers';
+import { FormFieldWrapper } from '@plone/volto/components';
+import { getVocabulary, getVocabularyTokenTitle } from '@plone/volto/actions';
+import { normalizeValue } from '@plone/volto/components/manage/Widgets/SelectUtils';
+
+import {
+ customSelectStyles,
+ DropdownIndicator,
+ ClearIndicator,
+ Option,
+ selectTheme,
+ MenuList,
+ MultiValueContainer,
+} from '@plone/volto/components/manage/Widgets/SelectStyling';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
+
+import loadable from '@loadable/component';
+
+export const Creatable = loadable(() => import('react-select/creatable'));
+
+const messages = defineMessages({
+ default: {
+ id: 'Default',
+ defaultMessage: 'Default',
+ },
+ idTitle: {
+ id: 'Short Name',
+ defaultMessage: 'Short Name',
+ },
+ idDescription: {
+ id: 'Used for programmatic access to the fieldset.',
+ defaultMessage: 'Used for programmatic access to the fieldset.',
+ },
+ title: {
+ id: 'Title',
+ defaultMessage: 'Title',
+ },
+ description: {
+ id: 'Description',
+ defaultMessage: 'Description',
+ },
+ close: {
+ id: 'Close',
+ defaultMessage: 'Close',
+ },
+ choices: {
+ id: 'Choices',
+ defaultMessage: 'Choices',
+ },
+ required: {
+ id: 'Required',
+ defaultMessage: 'Required',
+ },
+ select: {
+ id: 'Select…',
+ defaultMessage: 'Select…',
+ },
+ no_value: {
+ id: 'No value',
+ defaultMessage: 'No value',
+ },
+ no_options: {
+ id: 'No options',
+ defaultMessage: 'No options',
+ },
+});
+
+/**
+ * SelectWidget component class.
+ * @function SelectWidget
+ * @returns {string} Markup of the component.
+ */
+class SelectWidget extends Component {
+ /**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string,
+ required: PropTypes.bool,
+ error: PropTypes.arrayOf(PropTypes.string),
+ getVocabulary: PropTypes.func.isRequired,
+ getVocabularyTokenTitle: PropTypes.func.isRequired,
+ choices: PropTypes.arrayOf(
+ PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+ ),
+ items: PropTypes.shape({
+ vocabulary: PropTypes.object,
+ }),
+ widgetOptions: PropTypes.shape({
+ vocabulary: PropTypes.object,
+ }),
+ value: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.string,
+ PropTypes.bool,
+ PropTypes.func,
+ PropTypes.array,
+ ]),
+ onChange: PropTypes.func.isRequired,
+ onBlur: PropTypes.func,
+ onClick: PropTypes.func,
+ onEdit: PropTypes.func,
+ onDelete: PropTypes.func,
+ wrapped: PropTypes.bool,
+ noValueOption: PropTypes.bool,
+ customOptionStyling: PropTypes.any,
+ isMulti: PropTypes.bool,
+ placeholder: PropTypes.string,
+ };
+
+ /**
+ * Default properties
+ * @property {Object} defaultProps Default properties.
+ * @static
+ */
+ static defaultProps = {
+ description: null,
+ required: false,
+ items: {
+ vocabulary: null,
+ },
+ widgetOptions: {
+ vocabulary: null,
+ },
+ error: [],
+ choices: [],
+ value: null,
+ onChange: () => {},
+ onBlur: () => {},
+ onClick: () => {},
+ onEdit: null,
+ onDelete: null,
+ noValueOption: true,
+ customOptionStyling: null,
+ };
+
+ /**
+ * Component did mount
+ * @method componentDidMount
+ * @returns {undefined}
+ */
+ componentDidMount() {
+ if (
+ (!this.props.choices || this.props.choices?.length === 0) &&
+ this.props.vocabBaseUrl
+ ) {
+ this.props.getVocabulary({
+ vocabNameOrURL: this.props.vocabBaseUrl,
+ size: -1,
+ subrequest: this.props.lang,
+ });
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (
+ this.props.vocabBaseUrl !== prevProps.vocabBaseUrl &&
+ (!this.props.choices || this.props.choices?.length === 0)
+ ) {
+ this.props.getVocabulary({
+ vocabNameOrURL: this.props.vocabBaseUrl,
+ size: -1,
+ subrequest: this.props.lang,
+ });
+ }
+ }
+
+ /**
+ * Render method.
+ * @method render
+ * @returns {string} Markup for the component.
+ */
+ render() {
+ const { id, choices, value, intl, onChange } = this.props;
+ const normalizedValue = normalizeValue(choices, value, intl);
+ // Make sure that both disabled and isDisabled (from the DX layout feat work)
+ const disabled = this.props.disabled || this.props.isDisabled;
+
+ let options = this.props.vocabBaseUrl
+ ? this.props.choices
+ : [
+ ...map(choices, (option) => ({
+ value: option[0],
+ label:
+ // Fix "None" on the serializer, to remove when fixed in p.restapi
+ option[1] !== 'None' && option[1] ? option[1] : option[0],
+ })),
+ // Only set "no-value" option if there's no default in the field
+ // TODO: also if this.props.defaultValue?
+ ...(this.props.noValueOption &&
+ (this.props.default === undefined || this.props.default === null)
+ ? [
+ {
+ label: this.props.intl.formatMessage(messages.no_value),
+ value: 'no-value',
+ },
+ ]
+ : []),
+ ];
+
+ return (
+
+ 25 && {
+ MenuList,
+ }),
+ MultiValueContainer,
+ DropdownIndicator,
+ ClearIndicator,
+ Option: this.props.customOptionStyling || Option,
+ }}
+ value={normalizedValue}
+ placeholder={
+ this.props.placeholder ??
+ this.props.intl.formatMessage(messages.select)
+ }
+ onChange={(selectedOption) => {
+ return onChange(
+ id,
+ selectedOption.map((el) => el.value),
+ );
+ }}
+ isClearable
+ />
+
+ );
+ }
+}
+
+export const SelectWidgetComponent = injectIntl(SelectWidget);
+
+export default compose(
+ injectLazyLibs(['reactSelect']),
+ connect(
+ (state, props) => {
+ const vocabBaseUrl = !props.choices
+ ? getVocabFromHint(props) ||
+ getVocabFromField(props) ||
+ getVocabFromItems(props)
+ : '';
+
+ const vocabState =
+ state.vocabularies?.[vocabBaseUrl]?.subrequests?.[state.intl.locale];
+
+ // If the schema already has the choices in it, then do not try to get the vocab,
+ // even if there is one
+ if (props.choices) {
+ return {
+ choices: props.choices,
+ lang: state.intl.locale,
+ };
+ } else if (vocabState) {
+ return {
+ vocabBaseUrl,
+ choices: vocabState?.items ?? [],
+ lang: state.intl.locale,
+ };
+ // There is a moment that vocabState is not there yet, so we need to pass the
+ // vocabBaseUrl to the component.
+ } else if (vocabBaseUrl) {
+ return {
+ vocabBaseUrl,
+ lang: state.intl.locale,
+ };
+ }
+ return { lang: state.intl.locale };
+ },
+ { getVocabulary, getVocabularyTokenTitle },
+ ),
+)(SelectWidgetComponent);
diff --git a/src/components/theme/Widgets/CreatableSelectWidget.test.jsx b/src/components/theme/Widgets/CreatableSelectWidget.test.jsx
new file mode 100644
index 00000000..d72408d5
--- /dev/null
+++ b/src/components/theme/Widgets/CreatableSelectWidget.test.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import { Provider } from 'react-intl-redux';
+import { waitFor, render, screen, fireEvent } from '@testing-library/react';
+
+import CreatableSelectWidget from './CreatableSelectWidget';
+
+const mockStore = configureStore();
+
+jest.mock('@plone/volto/helpers/Loadable/Loadable');
+beforeAll(
+ async () =>
+ await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
+);
+
+test('renders a select widget component', async () => {
+ const store = mockStore({
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+ vocabularies: {
+ 'plone.app.vocabularies.Keywords': {
+ items: [{ title: 'My item', value: 'myitem' }],
+ itemsTotal: 1,
+ },
+ },
+ });
+
+ const { container } = render(
+
+ {}}
+ onBlur={() => {}}
+ onClick={() => {}}
+ />
+ ,
+ );
+
+ await waitFor(() => screen.getByText('My field'));
+ expect(container).toBeTruthy();
+});
+
+test("No 'No value' option when default value is 0", async () => {
+ const store = mockStore({
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+ });
+
+ const choices = [
+ ['0', 'None'],
+ ['1', 'One'],
+ ];
+
+ const value = {
+ value: '0',
+ label: 'None',
+ };
+
+ const _default = 0;
+
+ const { container } = render(
+
+ {}}
+ onBlur={() => {}}
+ onClick={() => {}}
+ />
+ ,
+ );
+
+ await waitFor(() => screen.getByText('None'));
+ fireEvent.mouseDown(
+ container.querySelector('.react-select__dropdown-indicator'),
+ { button: 0 },
+ );
+ expect(container).toBeTruthy();
+});
diff --git a/src/customizations/volto/components/manage/Blocks/Image/View.jsx b/src/customizations/volto/components/manage/Blocks/Image/View.jsx
index b1c86aae..3814856d 100644
--- a/src/customizations/volto/components/manage/Blocks/Image/View.jsx
+++ b/src/customizations/volto/components/manage/Blocks/Image/View.jsx
@@ -20,7 +20,7 @@ import { Copyright } from '@eeacms/volto-eea-design-system/ui';
*/
export const View = (props) => {
const { data, detached } = props;
- const href = data?.href?.[0]?.['@id'] ?? (data?.href || '');
+ const href = data?.href?.[0]?.['@id'] || '';
const { copyright, copyrightIcon, copyrightPosition } = data;
// const [hovering, setHovering] = React.useState(false);
const [viewLoaded, setViewLoaded] = React.useState(false);
diff --git a/src/helpers/schema-utils.js b/src/helpers/schema-utils.js
index 360203b3..3b784ab3 100644
--- a/src/helpers/schema-utils.js
+++ b/src/helpers/schema-utils.js
@@ -66,7 +66,13 @@ export const getVoltoStyles = (props) => {
if (styles[key] === true) {
output[key] = key;
} else {
- output[value] = value;
+ if (Array.isArray(value)) {
+ value.forEach((el, i) => {
+ output[el] = el;
+ });
+ } else {
+ output[value] = value;
+ }
}
}
return output;
diff --git a/src/index.js b/src/index.js
index a49cbd01..93d56634 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,8 +6,9 @@ import HomePageView from '@eeacms/volto-eea-website-theme/components/theme/Homep
import NotFound from '@eeacms/volto-eea-website-theme/components/theme/NotFound/NotFound';
import { TokenWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TokenWidget';
import { TopicsWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TopicsWidget';
+import CreatableSelectWidget from '@eeacms/volto-eea-website-theme/components/theme/Widgets/CreatableSelectWidget';
+
import { Icon } from '@plone/volto/components';
-import { getBlocks } from '@plone/volto/helpers';
import { serializeNodesToText } from '@plone/volto-slate/editor/render';
import Tag from '@eeacms/volto-eea-design-system/ui/Tag/Tag';
@@ -301,26 +302,6 @@ const applyConfig = (config) => {
// Apply columns block customization
if (config.blocks.blocksConfig.columnsBlock) {
config.blocks.blocksConfig.columnsBlock.available_colors = eea.colors;
- config.blocks.blocksConfig.columnsBlock.tocEntries = (
- tocData,
- block = {},
- ) => {
- // integration with volto-block-toc
- const headlines = tocData.levels || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
- let entries = [];
- const sorted_column_blocks = getBlocks(block?.data || {});
- sorted_column_blocks.forEach((column_block) => {
- const sorted_blocks = getBlocks(column_block[1]);
- sorted_blocks.forEach((block) => {
- const { value, plaintext } = block[1];
- const type = value?.[0]?.type;
- if (headlines.includes(type)) {
- entries.push([parseInt(type.slice(1)), plaintext, block[0]]);
- }
- });
- });
- return entries;
- };
}
// Description block custom CSS
@@ -336,6 +317,7 @@ const applyConfig = (config) => {
config.widgets.views.id.topics = TopicsWidget;
config.widgets.views.id.subjects = TokenWidget;
config.widgets.views.widget.tags = TokenWidget;
+ config.widgets.widget.creatable_select = CreatableSelectWidget;
// /voltoCustom.css express-middleware
// /ok express-middleware - see also: https://github.com/plone/volto/pull/4432
diff --git a/src/index.test.js b/src/index.test.js
index 1758381b..01a7aee1 100644
--- a/src/index.test.js
+++ b/src/index.test.js
@@ -91,6 +91,7 @@ describe('applyConfig', () => {
tags: undefined,
},
},
+ widget: {},
},
settings: {
eea: {
@@ -243,6 +244,7 @@ describe('applyConfig', () => {
tags: undefined,
},
},
+ widget: {},
},
settings: {
eea: {},