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(); + } + }} + > +
+ { + const { source, destination } = result; + if (!destination) { + return; + } + const newFormData = moveBlock( + properties, + source.index, + destination.index, + ); + onChangeFormData(newFormData); + return true; + }} + direction={direction} + > + {(dragProps) => { + const { child, childId, index } = dragProps; + const blockProps = { + allowedBlocks, + showRestricted, + block: childId, + data: child, + handleKeyDown, + id: childId, + formTitle: title, + formDescription: description, + index, + manage, + onAddBlock, + onInsertBlock, + onChangeBlock, + onChangeField, + onChangeFormData, + onDeleteBlock, + onFocusNextBlock, + onFocusPreviousBlock, + onMoveBlock, + onMutateBlock, + onSelectBlock, + errors, + pathname, + metadata, + properties, + contentType: type, + navRoot, + blocksConfig, + selected: selectedBlock === childId, + multiSelected: multiSelected?.includes(childId), + type: child['@type'], + editable, + showBlockChooser: selectedBlock === childId, + detached: isContainer, + }; + return editBlockWrapper( + dragProps, + , + blockProps, + ); + }} + +
+
+ ); +}; + +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) => ( + +
+ + {fieldset.title} + {metadataFieldsets.includes(fieldset.id) ? ( + + ) : ( + + )} + + + + {map(fieldset.fields, (field, index) => ( + + ))} + + +
+
+ ))} +
+
+ )} +
+ ) + ) : ( + + 0} + className={settings.verticalFormTabs ? 'vertical-form' : ''} + > +
+ + {schema && schema.fieldsets.length > 1 && ( + <> + {settings.verticalFormTabs && this.props.title && ( + + {this.props.title} + + )} + ({ + menuItem: item.title, + render: () => [ + !settings.verticalFormTabs && this.props.title && ( + + {this.props.title} + + ), + item.description && ( + + {item.description} + + ), + ...map(item.fields, (field, index) => ( + + )), + ], + }))} + /> + + )} + {schema && schema.fieldsets.length === 1 && ( + + {this.props.title && ( + +

{this.props.title}

+
+ )} + {this.props.description && ( + {this.props.description} + )} + {keys(this.state.errors).length > 0 && ( + + )} + {this.props.error && ( + + )} + {map(schema.fieldsets[0].fields, (field) => ( + + ))} +
+ )} + {!this.props.hideActions && ( + + {onSubmit && ( + + )} + {onCancel && ( + + )} + + )} +
+
+
+
+ ); + } +} + +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",