From 9e116563111b4e741678db21e7edf72ca6f163c6 Mon Sep 17 00:00:00 2001
From: dobri1408 <50819975+dobri1408@users.noreply.github.com>
Date: Mon, 5 Aug 2024 15:47:43 +0300
Subject: [PATCH 1/3] feat: Customization Pass errors blocksform - refs 269086
---
README.md | 2 +
.../manage/Blocks/Block/BlocksForm.diff | 12 +
.../manage/Blocks/Block/BlocksForm.jsx | 289 ++++++
.../manage/Blocks/Block/BlocksForm.txt | 1 +
.../volto/components/manage/Form/Form.diff | 2 +
.../volto/components/manage/Form/Form.jsx | 952 ++++++++++++++++++
.../volto/components/manage/Form/Form.txt | 1 +
7 files changed, 1259 insertions(+)
create mode 100644 src/customizations/volto/components/manage/Blocks/Block/BlocksForm.diff
create mode 100644 src/customizations/volto/components/manage/Blocks/Block/BlocksForm.jsx
create mode 100644 src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt
create mode 100644 src/customizations/volto/components/manage/Form/Form.diff
create mode 100644 src/customizations/volto/components/manage/Form/Form.jsx
create mode 100644 src/customizations/volto/components/manage/Form/Form.txt
diff --git a/README.md b/README.md
index 19814126..6e42a64a 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,8 @@ See [Storybook](https://eea.github.io/eea-storybook/).
**!!IMPORTANT**: This change requires volto@^16.26.1
- `volto/components/manage/Sidebar/SidebarPopup` -> https://github.com/plone/volto/pull/5520
+- `volto/components/manage/Form/Form.jsx` -> Pass errors of metadata validation to BlocksForm
+- `volto/components/manage/Blocks/Block/BlocksForm.jsx` -> Pass errors of metadata validation to blocks.
## Getting started
diff --git a/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.diff b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.diff
new file mode 100644
index 00000000..841d2107
--- /dev/null
+++ b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.diff
@@ -0,0 +1,12 @@
+3c3
+< import EditBlock from './Edit';
+---
+> import EditBlock from '@plone/volto/components/manage/Blocks/Block/Edit.jsx';
+20c20
+< import EditBlockWrapper from './EditBlockWrapper';
+---
+> import EditBlockWrapper from '@plone/volto/components/manage/Blocks/Block/EditBlockWrapper.jsx';
+41a42
+> errors,
+261a263
+> errors,
diff --git a/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.jsx b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.jsx
new file mode 100644
index 00000000..1c261177
--- /dev/null
+++ b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.jsx
@@ -0,0 +1,289 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import EditBlock from '@plone/volto/components/manage/Blocks/Block/Edit.jsx';
+import { DragDropList } from '@plone/volto/components';
+import {
+ getBlocks,
+ getBlocksFieldname,
+ applyBlockDefaults,
+} from '@plone/volto/helpers';
+import {
+ addBlock,
+ insertBlock,
+ changeBlock,
+ deleteBlock,
+ moveBlock,
+ mutateBlock,
+ nextBlockId,
+ previousBlockId,
+} from '@plone/volto/helpers';
+import EditBlockWrapper from '@plone/volto/components/manage/Blocks/Block/EditBlockWrapper.jsx';
+import { setSidebarTab } from '@plone/volto/actions';
+import { useDispatch } from 'react-redux';
+import { useDetectClickOutside, useEvent } from '@plone/volto/helpers';
+import config from '@plone/volto/registry';
+
+const BlocksForm = (props) => {
+ const {
+ pathname,
+ onChangeField,
+ properties,
+ type,
+ navRoot,
+ onChangeFormData,
+ selectedBlock,
+ multiSelected,
+ onSelectBlock,
+ allowedBlocks,
+ showRestricted,
+ title,
+ description,
+ metadata,
+ errors,
+ manage,
+ children,
+ isMainForm = true,
+ isContainer,
+ stopPropagation,
+ disableAddBlockOnEnterKey,
+ blocksConfig = config.blocks.blocksConfig,
+ editable = true,
+ direction = 'vertical',
+ } = props;
+
+ const blockList = getBlocks(properties);
+
+ const dispatch = useDispatch();
+ const intl = useIntl();
+
+ const ClickOutsideListener = () => {
+ onSelectBlock(null);
+ dispatch(setSidebarTab(0));
+ };
+
+ const ref = useDetectClickOutside({
+ onTriggered: ClickOutsideListener,
+ triggerKeys: ['Escape'],
+ // Disabled feature for now https://github.com/plone/volto/pull/2389#issuecomment-830027413
+ disableClick: true,
+ disableKeys: !isMainForm,
+ });
+
+ const handleKeyDown = (
+ e,
+ index,
+ block,
+ node,
+ {
+ disableEnter = false,
+ disableArrowUp = false,
+ disableArrowDown = false,
+ } = {},
+ ) => {
+ const isMultipleSelection = e.shiftKey;
+ if (e.key === 'ArrowUp' && !disableArrowUp) {
+ onFocusPreviousBlock(block, node, isMultipleSelection);
+ e.preventDefault();
+ }
+ if (e.key === 'ArrowDown' && !disableArrowDown) {
+ onFocusNextBlock(block, node, isMultipleSelection);
+ e.preventDefault();
+ }
+ if (e.key === 'Enter' && !disableEnter) {
+ if (!disableAddBlockOnEnterKey) {
+ onSelectBlock(onAddBlock(config.settings.defaultBlockType, index + 1));
+ }
+ e.preventDefault();
+ }
+ };
+
+ const onFocusPreviousBlock = (
+ currentBlock,
+ blockNode,
+ isMultipleSelection,
+ ) => {
+ const prev = previousBlockId(properties, currentBlock);
+ if (prev === null) return;
+
+ blockNode.blur();
+
+ onSelectBlock(prev, isMultipleSelection);
+ };
+
+ const onFocusNextBlock = (currentBlock, blockNode, isMultipleSelection) => {
+ const next = nextBlockId(properties, currentBlock);
+ if (next === null) return;
+
+ blockNode.blur();
+
+ onSelectBlock(next, isMultipleSelection);
+ };
+
+ const onMutateBlock = (id, value) => {
+ const newFormData = mutateBlock(properties, id, value);
+ onChangeFormData(newFormData);
+ };
+
+ const onInsertBlock = (id, value, current) => {
+ const [newId, newFormData] = insertBlock(
+ properties,
+ id,
+ value,
+ current,
+ config.experimental.addBlockButton.enabled ? 1 : 0,
+ );
+
+ const blocksFieldname = getBlocksFieldname(newFormData);
+ const blockData = newFormData[blocksFieldname][newId];
+ newFormData[blocksFieldname][newId] = applyBlockDefaults({
+ data: blockData,
+ intl,
+ metadata,
+ properties,
+ });
+
+ onChangeFormData(newFormData);
+ return newId;
+ };
+
+ const onAddBlock = (type, index) => {
+ if (editable) {
+ const [id, newFormData] = addBlock(properties, type, index);
+ const blocksFieldname = getBlocksFieldname(newFormData);
+ const blockData = newFormData[blocksFieldname][id];
+ newFormData[blocksFieldname][id] = applyBlockDefaults({
+ data: blockData,
+ intl,
+ metadata,
+ properties,
+ });
+ onChangeFormData(newFormData);
+ return id;
+ }
+ };
+
+ const onChangeBlock = (id, value) => {
+ const newFormData = changeBlock(properties, id, value);
+ onChangeFormData(newFormData);
+ };
+
+ const onDeleteBlock = (id, selectPrev) => {
+ const previous = previousBlockId(properties, id);
+
+ const newFormData = deleteBlock(properties, id);
+ onChangeFormData(newFormData);
+
+ onSelectBlock(selectPrev ? previous : null);
+ };
+
+ const onMoveBlock = (dragIndex, hoverIndex) => {
+ const newFormData = moveBlock(properties, dragIndex, hoverIndex);
+ onChangeFormData(newFormData);
+ };
+
+ const defaultBlockWrapper = ({ draginfo }, editBlock, blockProps) => (
+
+ {editBlock}
+
+ );
+
+ const editBlockWrapper = children || defaultBlockWrapper;
+
+ // Remove invalid blocks on saving
+ // Note they are alreaady filtered by DragDropList, but we also want them
+ // to be removed when the user saves the page next. Otherwise the invalid
+ // blocks would linger for ever.
+ for (const [n, v] of blockList) {
+ if (!v) {
+ const newFormData = deleteBlock(properties, n);
+ onChangeFormData(newFormData);
+ }
+ }
+
+ useEvent('voltoClickBelowContent', () => {
+ if (!config.experimental.addBlockButton.enabled || !isMainForm) return;
+ onSelectBlock(
+ onAddBlock(config.settings.defaultBlockType, blockList.length),
+ );
+ });
+
+ return (
+
{
+ if (stopPropagation) {
+ e.stopPropagation();
+ }
+ }}
+ >
+
+
+ );
+};
+
+export default BlocksForm;
diff --git a/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt
new file mode 100644
index 00000000..f95d1352
--- /dev/null
+++ b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt
@@ -0,0 +1 @@
+Customize from @plone/volto 17.18.2 - refs https://taskman.eionet.europa.eu/issues/269086
diff --git a/src/customizations/volto/components/manage/Form/Form.diff b/src/customizations/volto/components/manage/Form/Form.diff
new file mode 100644
index 00000000..e47f54e4
--- /dev/null
+++ b/src/customizations/volto/components/manage/Form/Form.diff
@@ -0,0 +1,2 @@
+697a698
+> errors={this.state.errors}
diff --git a/src/customizations/volto/components/manage/Form/Form.jsx b/src/customizations/volto/components/manage/Form/Form.jsx
new file mode 100644
index 00000000..376287bf
--- /dev/null
+++ b/src/customizations/volto/components/manage/Form/Form.jsx
@@ -0,0 +1,952 @@
+/**
+ * Form component.
+ * @module components/manage/Form/Form
+ */
+
+import { BlocksForm, Field, Icon, Toast } from '@plone/volto/components';
+import {
+ difference,
+ FormValidation,
+ getBlocksFieldname,
+ getBlocksLayoutFieldname,
+ messages,
+} from '@plone/volto/helpers';
+import aheadSVG from '@plone/volto/icons/ahead.svg';
+import clearSVG from '@plone/volto/icons/clear.svg';
+import upSVG from '@plone/volto/icons/up-key.svg';
+import downSVG from '@plone/volto/icons/down-key.svg';
+import {
+ findIndex,
+ isEmpty,
+ isEqual,
+ keys,
+ map,
+ mapValues,
+ pickBy,
+ without,
+ cloneDeep,
+ xor,
+} from 'lodash';
+import isBoolean from 'lodash/isBoolean';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { injectIntl } from 'react-intl';
+import { Portal } from 'react-portal';
+import { connect } from 'react-redux';
+import {
+ Accordion,
+ Button,
+ Container as SemanticContainer,
+ Form as UiForm,
+ Message,
+ Segment,
+ Tab,
+} from 'semantic-ui-react';
+import { v4 as uuid } from 'uuid';
+import { toast } from 'react-toastify';
+import { BlocksToolbar, UndoToolbar } from '@plone/volto/components';
+import {
+ setMetadataFieldsets,
+ resetMetadataFocus,
+ setSidebarTab,
+ setFormData,
+} from '@plone/volto/actions';
+import { compose } from 'redux';
+import config from '@plone/volto/registry';
+
+/**
+ * Form container class.
+ * @class Form
+ * @extends Component
+ */
+class Form extends Component {
+ /**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+ static propTypes = {
+ schema: PropTypes.shape({
+ fieldsets: PropTypes.arrayOf(
+ PropTypes.shape({
+ fields: PropTypes.arrayOf(PropTypes.string),
+ id: PropTypes.string,
+ title: PropTypes.string,
+ }),
+ ),
+ properties: PropTypes.objectOf(PropTypes.any),
+ definitions: PropTypes.objectOf(PropTypes.any),
+ required: PropTypes.arrayOf(PropTypes.string),
+ }),
+ formData: PropTypes.objectOf(PropTypes.any),
+ globalData: PropTypes.objectOf(PropTypes.any),
+ metadataFieldsets: PropTypes.arrayOf(PropTypes.string),
+ metadataFieldFocus: PropTypes.string,
+ pathname: PropTypes.string,
+ onSubmit: PropTypes.func,
+ onCancel: PropTypes.func,
+ submitLabel: PropTypes.string,
+ resetAfterSubmit: PropTypes.bool,
+ resetOnCancel: PropTypes.bool,
+ isEditForm: PropTypes.bool,
+ isAdminForm: PropTypes.bool,
+ title: PropTypes.string,
+ error: PropTypes.shape({
+ message: PropTypes.string,
+ }),
+ loading: PropTypes.bool,
+ hideActions: PropTypes.bool,
+ description: PropTypes.string,
+ visual: PropTypes.bool,
+ blocks: PropTypes.arrayOf(PropTypes.object),
+ isFormSelected: PropTypes.bool,
+ onSelectForm: PropTypes.func,
+ editable: PropTypes.bool,
+ onChangeFormData: PropTypes.func,
+ requestError: PropTypes.string,
+ allowedBlocks: PropTypes.arrayOf(PropTypes.string),
+ showRestricted: PropTypes.bool,
+ global: PropTypes.bool,
+ };
+
+ /**
+ * Default properties.
+ * @property {Object} defaultProps Default properties.
+ * @static
+ */
+ static defaultProps = {
+ formData: null,
+ onSubmit: null,
+ onCancel: null,
+ submitLabel: null,
+ resetAfterSubmit: false,
+ resetOnCancel: false,
+ isEditForm: false,
+ isAdminForm: false,
+ title: null,
+ description: null,
+ error: null,
+ loading: null,
+ hideActions: false,
+ visual: false,
+ blocks: [],
+ pathname: '',
+ schema: {},
+ isFormSelected: true,
+ onSelectForm: null,
+ editable: true,
+ requestError: null,
+ allowedBlocks: null,
+ global: false,
+ };
+
+ /**
+ * Constructor
+ * @method constructor
+ * @param {Object} props Component properties
+ * @constructs Form
+ */
+ constructor(props) {
+ super(props);
+ const ids = {
+ title: uuid(),
+ text: uuid(),
+ };
+ let { formData, schema: originalSchema } = props;
+ const blocksFieldname = getBlocksFieldname(formData);
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
+
+ const schema = this.removeBlocksLayoutFields(originalSchema);
+
+ this.props.setMetadataFieldsets(
+ schema?.fieldsets ? schema.fieldsets.map((fieldset) => fieldset.id) : [],
+ );
+
+ if (!props.isEditForm) {
+ // It's a normal (add form), get defaults from schema
+ formData = {
+ ...mapValues(props.schema.properties, 'default'),
+ ...formData,
+ };
+ }
+
+ // We initialize the formData snapshot in here, before the initial data checks
+ const initialFormData = cloneDeep(formData);
+
+ // Adding fallback in case the fields are empty, so we are sure that the edit form
+ // shows at least the default blocks
+ if (
+ formData.hasOwnProperty(blocksFieldname) &&
+ formData.hasOwnProperty(blocksLayoutFieldname)
+ ) {
+ if (
+ !formData[blocksLayoutFieldname] ||
+ isEmpty(formData[blocksLayoutFieldname].items)
+ ) {
+ formData[blocksLayoutFieldname] = {
+ items: [ids.title, ids.text],
+ };
+ }
+ if (!formData[blocksFieldname] || isEmpty(formData[blocksFieldname])) {
+ formData[blocksFieldname] = {
+ [ids.title]: {
+ '@type': 'title',
+ },
+ [ids.text]: {
+ '@type': config.settings.defaultBlockType,
+ },
+ };
+ }
+ }
+
+ let selectedBlock = null;
+ if (
+ formData.hasOwnProperty(blocksLayoutFieldname) &&
+ formData[blocksLayoutFieldname].items.length > 0
+ ) {
+ if (config.blocks?.initialBlocksFocus === null) {
+ selectedBlock = null;
+ } else if (this.props.type in config.blocks?.initialBlocksFocus) {
+ // Default selected is not the first block, but the one from config.
+ // TODO Select first block and not an arbitrary one.
+ Object.keys(formData[blocksFieldname]).forEach((b_key) => {
+ if (
+ formData[blocksFieldname][b_key]['@type'] ===
+ config.blocks?.initialBlocksFocus?.[this.props.type]
+ ) {
+ selectedBlock = b_key;
+ }
+ });
+ } else {
+ selectedBlock = formData[blocksLayoutFieldname].items[0];
+ }
+ }
+
+ // Sync state to global state
+ if (this.props.global) {
+ this.props.setFormData(formData);
+ }
+
+ // Set initial state
+ this.state = {
+ formData,
+ initialFormData,
+ errors: {},
+ selected: selectedBlock,
+ multiSelected: [],
+ isClient: false,
+ // Ensure focus remain in field after change
+ inFocus: {},
+ };
+ this.onChangeField = this.onChangeField.bind(this);
+ this.onSelectBlock = this.onSelectBlock.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ this.onCancel = this.onCancel.bind(this);
+ this.onTabChange = this.onTabChange.bind(this);
+ this.onBlurField = this.onBlurField.bind(this);
+ this.onClickInput = this.onClickInput.bind(this);
+ this.onToggleMetadataFieldset = this.onToggleMetadataFieldset.bind(this);
+ }
+
+ /**
+ * On updates caused by props change
+ * if errors from Backend come, these will be shown to their corresponding Fields
+ * also the first Tab to have any errors will be selected
+ * @param {Object} prevProps
+ */
+ async componentDidUpdate(prevProps, prevState) {
+ let { requestError } = this.props;
+ let errors = {};
+ let activeIndex = 0;
+
+ if (requestError && prevProps.requestError !== requestError) {
+ errors =
+ FormValidation.giveServerErrorsToCorrespondingFields(requestError);
+ activeIndex = FormValidation.showFirstTabWithErrors({
+ errors,
+ schema: this.props.schema,
+ });
+
+ this.setState({
+ errors,
+ activeIndex,
+ });
+ }
+
+ if (this.props.onChangeFormData) {
+ if (!isEqual(prevState?.formData, this.state.formData)) {
+ this.props.onChangeFormData(this.state.formData);
+ }
+ }
+ if (
+ this.props.global &&
+ !isEqual(this.props.globalData, prevProps.globalData)
+ ) {
+ this.setState({
+ formData: this.props.globalData,
+ });
+ }
+
+ if (!isEqual(prevProps.schema, this.props.schema)) {
+ this.props.setMetadataFieldsets(
+ this.removeBlocksLayoutFields(this.props.schema).fieldsets.map(
+ (fieldset) => fieldset.id,
+ ),
+ );
+ }
+
+ if (
+ this.props.metadataFieldFocus !== '' &&
+ !isEqual(prevProps.metadataFieldFocus, this.props.metadataFieldFocus)
+ ) {
+ // Scroll into view
+ document
+ .querySelector(`.field-wrapper-${this.props.metadataFieldFocus}`)
+ .scrollIntoView();
+
+ // Set focus to first input if available
+ document
+ .querySelector(`.field-wrapper-${this.props.metadataFieldFocus} input`)
+ .focus();
+
+ // Reset focus field
+ this.props.resetMetadataFocus();
+ }
+ }
+
+ /**
+ * Tab selection is done only by setting activeIndex in state
+ */
+ onTabChange(e, { activeIndex }) {
+ const defaultFocus = this.props.schema.fieldsets[activeIndex].fields[0];
+ this.setState({
+ activeIndex,
+ ...(defaultFocus ? { inFocus: { [defaultFocus]: true } } : {}),
+ });
+ }
+
+ /**
+ * If user clicks on input, the form will be not considered pristine
+ * this will avoid onBlur effects without interraction with the form
+ * @param {Object} e event
+ */
+ onClickInput(e) {
+ this.setState({ isFormPristine: false });
+ }
+
+ /**
+ * Validate fields on blur
+ * @method onBlurField
+ * @param {string} id Id of the field
+ * @param {*} value Value of the field
+ * @returns {undefined}
+ */
+ onBlurField(id, value) {
+ if (!this.state.isFormPristine) {
+ const errors = FormValidation.validateFieldsPerFieldset({
+ schema: this.props.schema,
+ formData: this.state.formData,
+ formatMessage: this.props.intl.formatMessage,
+ touchedField: { [id]: value },
+ });
+
+ this.setState({
+ errors,
+ });
+ }
+ }
+
+ /**
+ * Component did mount
+ * @method componentDidMount
+ * @returns {undefined}
+ */
+ componentDidMount() {
+ this.setState({ isClient: true });
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ let newState = { ...state };
+ if (!props.isFormSelected) {
+ newState.selected = null;
+ }
+
+ return newState;
+ }
+
+ /**
+ * Change field handler
+ * Remove errors for changed field
+ * @method onChangeField
+ * @param {string} id Id of the field
+ * @param {*} value Value of the field
+ * @returns {undefined}
+ */
+ onChangeField(id, value) {
+ this.setState((prevState) => {
+ const { errors, formData } = prevState;
+ const newFormData = {
+ ...formData,
+ // We need to catch also when the value equals false this fixes #888
+ [id]: value || (value !== undefined && isBoolean(value)) ? value : null,
+ };
+ delete errors[id];
+ if (this.props.global) {
+ this.props.setFormData(newFormData);
+ }
+ return {
+ errors,
+ formData: newFormData,
+ // Changing the form data re-renders the select widget which causes the
+ // focus to get lost. To circumvent this, we set the focus back to
+ // the input.
+ // This could fix other widgets too but currently targeted
+ // against the select widget only.
+ // Ensure field to be in focus after the change
+ inFocus: { [id]: true },
+ };
+ });
+ }
+
+ /**
+ * Select block handler
+ * @method onSelectBlock
+ * @param {string} id Id of the field
+ * @param {string} isMultipleSelection true if multiple blocks are selected
+ * @returns {undefined}
+ */
+ onSelectBlock(id, isMultipleSelection, event) {
+ let multiSelected = [];
+ let selected = id;
+ const formData = this.state.formData;
+
+ if (isMultipleSelection) {
+ selected = null;
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
+
+ const blocks_layout = formData[blocksLayoutFieldname].items;
+
+ if (event.shiftKey) {
+ const anchor =
+ this.state.multiSelected.length > 0
+ ? blocks_layout.indexOf(this.state.multiSelected[0])
+ : blocks_layout.indexOf(this.state.selected);
+ const focus = blocks_layout.indexOf(id);
+
+ if (anchor === focus) {
+ multiSelected = [id];
+ } else if (focus > anchor) {
+ multiSelected = [...blocks_layout.slice(anchor, focus + 1)];
+ } else {
+ multiSelected = [...blocks_layout.slice(focus, anchor + 1)];
+ }
+ }
+
+ if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
+ multiSelected = this.state.multiSelected || [];
+ if (!this.state.multiSelected.includes(this.state.selected)) {
+ multiSelected = [...multiSelected, this.state.selected];
+ selected = null;
+ }
+ if (this.state.multiSelected.includes(id)) {
+ selected = null;
+ multiSelected = without(multiSelected, id);
+ } else {
+ multiSelected = [...multiSelected, id];
+ }
+ }
+ }
+
+ this.setState({
+ selected,
+ multiSelected,
+ });
+
+ if (this.props.onSelectForm) {
+ if (event) event.nativeEvent.stopImmediatePropagation();
+ this.props.onSelectForm();
+ }
+ }
+
+ /**
+ * Cancel handler
+ * It prevents event from triggering submit, reset form if props.resetAfterSubmit
+ * and calls this.props.onCancel
+ * @method onCancel
+ * @param {Object} event Event object.
+ * @returns {undefined}
+ */
+ onCancel(event) {
+ if (event) {
+ event.preventDefault();
+ }
+ if (this.props.resetOnCancel || this.props.resetAfterSubmit) {
+ this.setState({
+ formData: this.props.formData,
+ });
+ if (this.props.global) {
+ this.props.setFormData(this.props.formData);
+ }
+ }
+ this.props.onCancel(event);
+ }
+
+ /**
+ * Submit handler also validate form and collect errors
+ * @method onSubmit
+ * @param {Object} event Event object.
+ * @returns {undefined}
+ */
+ onSubmit(event) {
+ const formData = this.state.formData;
+
+ if (event) {
+ event.preventDefault();
+ }
+
+ const errors = this.props.schema
+ ? FormValidation.validateFieldsPerFieldset({
+ schema: this.props.schema,
+ formData,
+ formatMessage: this.props.intl.formatMessage,
+ })
+ : {};
+
+ if (keys(errors).length > 0) {
+ const activeIndex = FormValidation.showFirstTabWithErrors({
+ errors,
+ schema: this.props.schema,
+ });
+ this.setState(
+ {
+ errors,
+ activeIndex,
+ },
+ () => {
+ Object.keys(errors).forEach((err) =>
+ toast.error(
+ ,
+ ),
+ );
+ },
+ );
+ // Changes the focus to the metadata tab in the sidebar if error
+ this.props.setSidebarTab(0);
+ } else {
+ // Get only the values that have been modified (Edit forms), send all in case that
+ // it's an add form
+ if (this.props.isEditForm) {
+ this.props.onSubmit(this.getOnlyFormModifiedValues());
+ } else {
+ this.props.onSubmit(formData);
+ }
+ if (this.props.resetAfterSubmit) {
+ this.setState({
+ formData: this.props.formData,
+ });
+ if (this.props.global) {
+ this.props.setFormData(this.props.formData);
+ }
+ }
+ }
+ }
+
+ /**
+ * getOnlyFormModifiedValues handler
+ * It returns only the values of the fields that are have really changed since the
+ * form was loaded. Useful for edit forms and PATCH operations, when we only want to
+ * send the changed data.
+ * @method getOnlyFormModifiedValues
+ * @param {Object} event Event object.
+ * @returns {undefined}
+ */
+ getOnlyFormModifiedValues = () => {
+ const formData = this.state.formData;
+
+ const fieldsModified = Object.keys(
+ difference(formData, this.state.initialFormData),
+ );
+ return {
+ ...pickBy(formData, (value, key) => fieldsModified.includes(key)),
+ ...(formData['@static_behaviors'] && {
+ '@static_behaviors': formData['@static_behaviors'],
+ }),
+ };
+ };
+
+ /**
+ * Removed blocks and blocks_layout fields from the form.
+ * @method removeBlocksLayoutFields
+ * @param {object} schema The schema definition of the form.
+ * @returns A modified copy of the given schema.
+ */
+ removeBlocksLayoutFields = (schema) => {
+ const newSchema = { ...schema };
+ const layoutFieldsetIndex = findIndex(
+ newSchema.fieldsets,
+ (fieldset) => fieldset.id === 'layout',
+ );
+ if (layoutFieldsetIndex > -1) {
+ const layoutFields = newSchema.fieldsets[layoutFieldsetIndex].fields;
+ newSchema.fieldsets[layoutFieldsetIndex].fields = layoutFields.filter(
+ (field) => field !== 'blocks' && field !== 'blocks_layout',
+ );
+ if (newSchema.fieldsets[layoutFieldsetIndex].fields.length === 0) {
+ newSchema.fieldsets = [
+ ...newSchema.fieldsets.slice(0, layoutFieldsetIndex),
+ ...newSchema.fieldsets.slice(layoutFieldsetIndex + 1),
+ ];
+ }
+ }
+ return newSchema;
+ };
+
+ /**
+ * Toggle metadata fieldset handler
+ * @method onToggleMetadataFieldset
+ * @param {Object} event Event object.
+ * @param {Object} blockProps Block properties.
+ * @returns {undefined}
+ */
+ onToggleMetadataFieldset(event, blockProps) {
+ const { index } = blockProps;
+ this.props.setMetadataFieldsets(xor(this.props.metadataFieldsets, [index]));
+ }
+
+ /**
+ * Render method.
+ * @method render
+ * @returns {string} Markup for the component.
+ */
+ render() {
+ const { settings } = config;
+ const {
+ schema: originalSchema,
+ onCancel,
+ onSubmit,
+ navRoot,
+ type,
+ metadataFieldsets,
+ } = this.props;
+ const formData = this.state.formData;
+ const schema = this.removeBlocksLayoutFields(originalSchema);
+ const Container =
+ config.getComponent({ name: 'Container' }).component || SemanticContainer;
+
+ return this.props.visual ? (
+ // Removing this from SSR is important, since react-beautiful-dnd supports SSR,
+ // but draftJS don't like it much and the hydration gets messed up
+ this.state.isClient && (
+
+ {
+ const newFormData = {
+ ...formData,
+ ...newBlockData,
+ };
+ this.setState({
+ formData: newFormData,
+ });
+ if (this.props.global) {
+ this.props.setFormData(newFormData);
+ }
+ }}
+ onSetSelectedBlocks={(blockIds) =>
+ this.setState({ multiSelected: blockIds })
+ }
+ onSelectBlock={this.onSelectBlock}
+ />
+ {
+ if (this.props.global) {
+ this.props.setFormData(state.formData);
+ }
+ return this.setState(state);
+ }}
+ />
+ {
+ const newFormData = {
+ ...formData,
+ ...newData,
+ };
+ this.setState({
+ formData: newFormData,
+ });
+ if (this.props.global) {
+ this.props.setFormData(newFormData);
+ }
+ }}
+ onChangeField={this.onChangeField}
+ onSelectBlock={this.onSelectBlock}
+ properties={formData}
+ navRoot={navRoot}
+ type={type}
+ errors={this.state.errors}
+ pathname={this.props.pathname}
+ selectedBlock={this.state.selected}
+ multiSelected={this.state.multiSelected}
+ manage={this.props.isAdminForm}
+ allowedBlocks={this.props.allowedBlocks}
+ showRestricted={this.props.showRestricted}
+ editable={this.props.editable}
+ isMainForm={this.props.editable}
+ />
+ {this.state.isClient && this.props.editable && (
+
+ 0}
+ >
+ {schema &&
+ map(schema.fieldsets, (fieldset) => (
+
+
+
+ ))}
+
+
+ )}
+
+ )
+ ) : (
+
+ 0}
+ className={settings.verticalFormTabs ? 'vertical-form' : ''}
+ >
+
+
+
+ );
+ }
+}
+
+const FormIntl = injectIntl(Form, { forwardRef: true });
+
+export default compose(
+ connect(
+ (state, props) => ({
+ globalData: state.form?.global,
+ metadataFieldsets: state.sidebar?.metadataFieldsets,
+ metadataFieldFocus: state.sidebar?.metadataFieldFocus,
+ }),
+ {
+ setMetadataFieldsets,
+ setSidebarTab,
+ setFormData,
+ resetMetadataFocus,
+ },
+ null,
+ { forwardRef: true },
+ ),
+)(FormIntl);
diff --git a/src/customizations/volto/components/manage/Form/Form.txt b/src/customizations/volto/components/manage/Form/Form.txt
new file mode 100644
index 00000000..f95d1352
--- /dev/null
+++ b/src/customizations/volto/components/manage/Form/Form.txt
@@ -0,0 +1 @@
+Customize from @plone/volto 17.18.2 - refs https://taskman.eionet.europa.eu/issues/269086
From 1dc85ab3d5b3c9de788cd9dbce90284a503adddf Mon Sep 17 00:00:00 2001
From: alin
Date: Mon, 5 Aug 2024 15:50:40 +0300
Subject: [PATCH 2/3] Add more info to customized components
---
.../volto/components/manage/Blocks/Block/BlocksForm.txt | 1 +
src/customizations/volto/components/manage/Form/Form.txt | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt
index f95d1352..8836b916 100644
--- a/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt
+++ b/src/customizations/volto/components/manage/Blocks/Block/BlocksForm.txt
@@ -1 +1,2 @@
Customize from @plone/volto 17.18.2 - refs https://taskman.eionet.europa.eu/issues/269086
+Should be safe to remove with @plone/volto 18.x - https://github.com/plone/volto/pull/6181
diff --git a/src/customizations/volto/components/manage/Form/Form.txt b/src/customizations/volto/components/manage/Form/Form.txt
index f95d1352..8836b916 100644
--- a/src/customizations/volto/components/manage/Form/Form.txt
+++ b/src/customizations/volto/components/manage/Form/Form.txt
@@ -1 +1,2 @@
Customize from @plone/volto 17.18.2 - refs https://taskman.eionet.europa.eu/issues/269086
+Should be safe to remove with @plone/volto 18.x - https://github.com/plone/volto/pull/6181
From e3f24aa93ab68248a1f236c077a7348773c8f141 Mon Sep 17 00:00:00 2001
From: EEA Jenkins <@users.noreply.github.com>
Date: Mon, 5 Aug 2024 13:01:57 +0000
Subject: [PATCH 3/3] Automated release 2.1.5
---
CHANGELOG.md | 11 ++++++++---
package.json | 2 +-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97fae4d0..e65310da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,11 +4,16 @@ 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).
-### [2.1.4](https://github.com/eea/volto-eea-website-theme/compare/2.1.3...2.1.4) - 30 July 2024
+### [2.1.5](https://github.com/eea/volto-eea-website-theme/compare/2.1.4...2.1.5) - 5 August 2024
-#### :bug: Bug Fixes
+#### :rocket: New Features
+
+- feat: Customization Pass errors blocksform - refs 269086 [dobri1408 - [`9e11656`](https://github.com/eea/volto-eea-website-theme/commit/9e116563111b4e741678db21e7edf72ca6f163c6)]
+
+#### :hammer_and_wrench: Others
-- fix(slate): don't customize slate li element, ref #269872 [Miu Razvan - [`945afa5`](https://github.com/eea/volto-eea-website-theme/commit/945afa5c1076de4779d46f602300a61adf41937d)]
+- Add more info to customized components [alin - [`1dc85ab`](https://github.com/eea/volto-eea-website-theme/commit/1dc85ab3d5b3c9de788cd9dbce90284a503adddf)]
+### [2.1.4](https://github.com/eea/volto-eea-website-theme/compare/2.1.3...2.1.4) - 1 August 2024
### [2.1.3](https://github.com/eea/volto-eea-website-theme/compare/2.1.2...2.1.3) - 22 July 2024
diff --git a/package.json b/package.json
index 83454003..c9c128ef 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@eeacms/volto-eea-website-theme",
- "version": "2.1.4",
+ "version": "2.1.5",
"description": "@eeacms/volto-eea-website-theme: Volto add-on",
"main": "src/index.js",
"author": "European Environment Agency: IDM2 A-Team",