From 30f2508df978cd6e586084f25caef347c18e163d Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 23 Feb 2024 11:16:22 +0100 Subject: [PATCH 1/2] Added global form state (#5721) --- news/5721.feature | 1 + src/actions/form/form.js | 19 ++++ src/actions/form/form.test.js | 14 +++ src/actions/index.js | 1 + src/components/manage/Add/Add.jsx | 1 + src/components/manage/Edit/Edit.jsx | 1 + src/components/manage/Form/Form.jsx | 140 ++++++++++++++++++---------- src/constants/ActionTypes.js | 1 + src/reducers/form/form.js | 18 +++- src/reducers/form/form.test.js | 14 ++- 10 files changed, 158 insertions(+), 52 deletions(-) create mode 100644 news/5721.feature create mode 100644 src/actions/form/form.js create mode 100644 src/actions/form/form.test.js diff --git a/news/5721.feature b/news/5721.feature new file mode 100644 index 0000000000..03fe369cba --- /dev/null +++ b/news/5721.feature @@ -0,0 +1 @@ +Add global form state. @robgietema \ No newline at end of file diff --git a/src/actions/form/form.js b/src/actions/form/form.js new file mode 100644 index 0000000000..5cc22aabc3 --- /dev/null +++ b/src/actions/form/form.js @@ -0,0 +1,19 @@ +/** + * Form actions. + * @module actions/form/form + */ + +import { SET_FORM_DATA } from '@plone/volto/constants/ActionTypes'; + +/** + * Set form data function. + * @function setFormData + * @param {Object} data New form data. + * @returns {Object} Set sidebar action. + */ +export function setFormData(data) { + return { + type: SET_FORM_DATA, + data, + }; +} diff --git a/src/actions/form/form.test.js b/src/actions/form/form.test.js new file mode 100644 index 0000000000..422ace4bcb --- /dev/null +++ b/src/actions/form/form.test.js @@ -0,0 +1,14 @@ +import { setFormData } from './form'; +import { SET_FORM_DATA } from '@plone/volto/constants/ActionTypes'; + +describe('Form action', () => { + describe('setFormData', () => { + it('should create an action to set the form data', () => { + const data = { foo: 'bar' }; + const action = setFormData(data); + + expect(action.type).toEqual(SET_FORM_DATA); + expect(action.data).toEqual(data); + }); + }); +}); diff --git a/src/actions/index.js b/src/actions/index.js index c556018d21..7ab5eff91b 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -150,6 +150,7 @@ export { export { getQuerystring } from '@plone/volto/actions/querystring/querystring'; export { getQueryStringResults } from '@plone/volto/actions/querystringsearch/querystringsearch'; export { setSidebarTab } from '@plone/volto/actions/sidebar/sidebar'; +export { setFormData } from '@plone/volto/actions/form/form'; export { deleteLinkTranslation, getTranslationLocator, diff --git a/src/components/manage/Add/Add.jsx b/src/components/manage/Add/Add.jsx index 0b3ca9430b..1aababb253 100644 --- a/src/components/manage/Add/Add.jsx +++ b/src/components/manage/Add/Add.jsx @@ -359,6 +359,7 @@ class Add extends Component { onSelectForm={() => { this.setState({ formSelected: 'addForm' }); }} + global /> {this.state.isClient && ( diff --git a/src/components/manage/Edit/Edit.jsx b/src/components/manage/Edit/Edit.jsx index e8bef08ef2..c8fbb89761 100644 --- a/src/components/manage/Edit/Edit.jsx +++ b/src/components/manage/Edit/Edit.jsx @@ -307,6 +307,7 @@ class Edit extends Component { onSelectForm={() => { this.setState({ formSelected: 'editForm' }); }} + global /> ); diff --git a/src/components/manage/Form/Form.jsx b/src/components/manage/Form/Form.jsx index 38aa59861e..7b635139ef 100644 --- a/src/components/manage/Form/Form.jsx +++ b/src/components/manage/Form/Form.jsx @@ -16,6 +16,7 @@ import clearSVG from '@plone/volto/icons/clear.svg'; import { findIndex, isEmpty, + isEqual, keys, map, mapValues, @@ -40,7 +41,7 @@ import { import { v4 as uuid } from 'uuid'; import { toast } from 'react-toastify'; import { BlocksToolbar, UndoToolbar } from '@plone/volto/components'; -import { setSidebarTab } from '@plone/volto/actions'; +import { setSidebarTab, setFormData } from '@plone/volto/actions'; import { compose } from 'redux'; import config from '@plone/volto/registry'; @@ -69,6 +70,7 @@ class Form extends Component { required: PropTypes.arrayOf(PropTypes.string), }), formData: PropTypes.objectOf(PropTypes.any), + globalData: PropTypes.objectOf(PropTypes.any), pathname: PropTypes.string, onSubmit: PropTypes.func, onCancel: PropTypes.func, @@ -93,6 +95,7 @@ class Form extends Component { requestError: PropTypes.string, allowedBlocks: PropTypes.arrayOf(PropTypes.string), showRestricted: PropTypes.bool, + global: PropTypes.bool, }; /** @@ -123,6 +126,7 @@ class Form extends Component { editable: true, requestError: null, allowedBlocks: null, + global: false, }; /** @@ -198,6 +202,12 @@ class Form extends Component { } } + // Sync state to global state + if (this.props.global) { + this.props.setFormData(formData); + } + + // Set initial state this.state = { formData, initialFormData: cloneDeep(formData), @@ -244,14 +254,18 @@ class Form extends Component { } if (this.props.onChangeFormData) { - if ( - // TODO: use fast-deep-equal - JSON.stringify(prevState?.formData) !== - JSON.stringify(this.state.formData) - ) { + if (!isEqual(prevState?.formData, this.state.formData)) { this.props.onChangeFormData(this.state.formData); } } + if ( + this.props.global && + !isEqual(this.props.globalData, this.state.formData) + ) { + this.setState({ + formData: this.props.globalData, + }); + } } /** @@ -325,15 +339,18 @@ class Form extends Component { 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: { - ...formData, - // We need to catch also when the value equals false this fixes #888 - [id]: - value || (value !== undefined && isBoolean(value)) ? value : null, - }, + 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. @@ -355,14 +372,13 @@ class Form extends Component { onSelectBlock(id, isMultipleSelection, event) { let multiSelected = []; let selected = id; + const formData = this.state.formData; if (isMultipleSelection) { selected = null; - const blocksLayoutFieldname = getBlocksLayoutFieldname( - this.state.formData, - ); + const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); - const blocks_layout = this.state.formData[blocksLayoutFieldname].items; + const blocks_layout = formData[blocksLayoutFieldname].items; if (event.shiftKey) { const anchor = @@ -422,6 +438,9 @@ class Form extends Component { this.setState({ formData: this.props.formData, }); + if (this.props.global) { + this.props.setFormData(this.props.formData); + } } this.props.onCancel(event); } @@ -433,6 +452,8 @@ class Form extends Component { * @returns {undefined} */ onSubmit(event) { + const formData = this.state.formData; + if (event) { event.preventDefault(); } @@ -440,7 +461,7 @@ class Form extends Component { const errors = this.props.schema ? FormValidation.validateFieldsPerFieldset({ schema: this.props.schema, - formData: this.state.formData, + formData, formatMessage: this.props.intl.formatMessage, }) : {}; @@ -475,12 +496,15 @@ class Form extends Component { if (this.props.isEditForm) { this.props.onSubmit(this.getOnlyFormModifiedValues()); } else { - this.props.onSubmit(this.state.formData); + this.props.onSubmit(formData); } if (this.props.resetAfterSubmit) { this.setState({ formData: this.props.formData, }); + if (this.props.global) { + this.props.setFormData(this.props.formData); + } } } } @@ -495,15 +519,15 @@ class Form extends Component { * @returns {undefined} */ getOnlyFormModifiedValues = () => { + const formData = this.state.formData; + const fieldsModified = Object.keys( - difference(this.state.formData, this.state.initialFormData), + difference(formData, this.state.initialFormData), ); return { - ...pickBy(this.state.formData, (value, key) => - fieldsModified.includes(key), - ), - ...(this.state.formData['@static_behaviors'] && { - '@static_behaviors': this.state.formData['@static_behaviors'], + ...pickBy(formData, (value, key) => fieldsModified.includes(key)), + ...(formData['@static_behaviors'] && { + '@static_behaviors': formData['@static_behaviors'], }), }; }; @@ -549,7 +573,7 @@ class Form extends Component { navRoot, type, } = this.props; - const { formData } = this.state; + const formData = this.state.formData; const schema = this.removeBlocksLayoutFields(originalSchema); const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; @@ -560,17 +584,21 @@ class Form extends Component { this.state.isClient && ( + onChangeBlocks={(newBlockData) => { + const newFormData = { + ...formData, + ...newBlockData, + }; this.setState({ - formData: { - ...formData, - ...newBlockData, - }, - }) - } + formData: newFormData, + }); + if (this.props.global) { + this.props.setFormData(newFormData); + } + }} onSetSelectedBlocks={(blockIds) => this.setState({ multiSelected: blockIds }) } @@ -578,22 +606,31 @@ class Form extends Component { /> this.setState(state)} + onUndoRedo={({ state }) => { + if (this.props.global) { + this.props.setFormData(state.formData); + } + return this.setState(state); + }} /> + onChangeFormData={(newData) => { + const newFormData = { + ...formData, + ...newData, + }; this.setState({ - formData: { - ...formData, - ...newFormData, - }, - }) - } + formData: newFormData, + }); + if (this.props.global) { + this.props.setFormData(newFormData); + } + }} onChangeField={this.onChangeField} onSelectBlock={this.onSelectBlock} properties={formData} @@ -633,9 +670,9 @@ class Form extends Component { {...schema.properties[field]} id={field} fieldSet={item.title.toLowerCase()} - formData={this.state.formData} + formData={formData} focus={this.state.inFocus[field]} - value={this.state.formData?.[field]} + value={formData?.[field]} required={schema.required.indexOf(field) !== -1} onChange={this.onChangeField} onBlur={this.onBlurField} @@ -698,10 +735,10 @@ class Form extends Component { {...schema.properties[field]} isDisabled={!this.props.editable} id={field} - formData={this.state.formData} + formData={formData} fieldSet={item.title.toLowerCase()} focus={this.state.inFocus[field]} - value={this.state.formData?.[field]} + value={formData?.[field]} required={schema.required.indexOf(field) !== -1} onChange={this.onChangeField} onBlur={this.onBlurField} @@ -749,7 +786,7 @@ class Form extends Component { ({ + globalData: state.form?.global, + }), + { setSidebarTab, setFormData }, + null, + { forwardRef: true }, + ), )(FormIntl); diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js index bdd9b64c34..aa04e49422 100644 --- a/src/constants/ActionTypes.js +++ b/src/constants/ActionTypes.js @@ -138,3 +138,4 @@ export const REMOVE_ALIASES = 'REMOVE_ALIASES'; export const GET_USERSCHEMA = 'GET_USERSCHEMA'; export const GET_UPGRADE = 'GET_UPGRADE'; export const POST_UPGRADE = 'POST_UPGRADE'; +export const SET_FORM_DATA = 'SET_FORM_DATA'; diff --git a/src/reducers/form/form.js b/src/reducers/form/form.js index 65ba61fecd..8c6a59dc5d 100644 --- a/src/reducers/form/form.js +++ b/src/reducers/form/form.js @@ -4,7 +4,11 @@ * @module reducers/form/form */ -const initialState = {}; +import { SET_FORM_DATA } from '@plone/volto/constants/ActionTypes'; + +const initialState = { + global: {}, +}; /** * Form reducer. @@ -12,6 +16,14 @@ const initialState = {}; * @param {Object} state Current state. * @returns {Object} New state. */ -export default function form(state = initialState) { - return state; +export default function form(state = initialState, action = {}) { + switch (action.type) { + case SET_FORM_DATA: + return { + ...state, + global: action.data, + }; + default: + return state; + } } diff --git a/src/reducers/form/form.test.js b/src/reducers/form/form.test.js index 04baf6ec8a..313763c164 100644 --- a/src/reducers/form/form.test.js +++ b/src/reducers/form/form.test.js @@ -1,7 +1,19 @@ import form from './form'; +import { SET_FORM_DATA } from '@plone/volto/constants/ActionTypes'; describe('Form reducer', () => { it('should return the initial state', () => { - expect(form()).toEqual({}); + expect(form()).toEqual({ global: {} }); + }); + + it('should handle SET_FORM_DATA', () => { + expect( + form(undefined, { + type: SET_FORM_DATA, + data: { foo: 'bar' }, + }), + ).toEqual({ + global: { foo: 'bar' }, + }); }); }); From 9956479587d5cdb7f8ed080fa3359e43d83e07b8 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Thu, 22 Feb 2024 11:26:24 +0100 Subject: [PATCH 2/2] Fix sidebar form update. (#5779) --- packages/volto-slate/news/5779.bugfix | 1 + .../src/blocks/Table/TableBlockEdit.jsx | 16 +++++----------- packages/volto/news/5779.bugfix | 1 + src/components/manage/Form/Form.jsx | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 packages/volto-slate/news/5779.bugfix create mode 100644 packages/volto/news/5779.bugfix diff --git a/packages/volto-slate/news/5779.bugfix b/packages/volto-slate/news/5779.bugfix new file mode 100644 index 0000000000..5c1f633b25 --- /dev/null +++ b/packages/volto-slate/news/5779.bugfix @@ -0,0 +1 @@ +Fix sidebar form update. @robgietema \ No newline at end of file diff --git a/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx b/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx index 478ba15be9..397bff13fd 100644 --- a/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx +++ b/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { map, remove } from 'lodash'; +import { isEmpty, map, remove } from 'lodash'; import { Button, Table } from 'semantic-ui-react'; import cx from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; @@ -227,7 +227,7 @@ class Edit extends Component { * @returns {undefined} */ componentDidMount() { - if (!this.props.data.table) { + if (!this.props.data.table || isEmpty(this.props.data.table)) { this.props.onChangeBlock(this.props.block, { ...this.props.data, table: initialTable, @@ -243,7 +243,7 @@ class Edit extends Component { * @returns {undefined} */ UNSAFE_componentWillReceiveProps(nextProps) { - if (!nextProps.data.table) { + if (!nextProps.data.table || isEmpty(nextProps.data.table)) { this.props.onChangeBlock(nextProps.block, { ...nextProps.data, table: initialTable, @@ -528,10 +528,7 @@ class Edit extends Component { icon basic onClick={this.onDeleteRow} - disabled={ - this.props.data.table && - this.props.data.table.rows.length === 1 - } + disabled={this.props.data.table?.rows?.length === 1} title={this.props.intl.formatMessage(messages.deleteRow)} aria-label={this.props.intl.formatMessage(messages.deleteRow)} > @@ -569,10 +566,7 @@ class Edit extends Component { icon basic onClick={this.onDeleteCol} - disabled={ - this.props.data.table && - this.props.data.table.rows[0].cells.length === 1 - } + disabled={this.props.data.table?.rows?.[0].cells.length === 1} title={this.props.intl.formatMessage(messages.deleteCol)} aria-label={this.props.intl.formatMessage(messages.deleteCol)} > diff --git a/packages/volto/news/5779.bugfix b/packages/volto/news/5779.bugfix new file mode 100644 index 0000000000..5c1f633b25 --- /dev/null +++ b/packages/volto/news/5779.bugfix @@ -0,0 +1 @@ +Fix sidebar form update. @robgietema \ No newline at end of file diff --git a/src/components/manage/Form/Form.jsx b/src/components/manage/Form/Form.jsx index 7b635139ef..af6d1ceaca 100644 --- a/src/components/manage/Form/Form.jsx +++ b/src/components/manage/Form/Form.jsx @@ -260,7 +260,7 @@ class Form extends Component { } if ( this.props.global && - !isEqual(this.props.globalData, this.state.formData) + !isEqual(this.props.globalData, prevProps.globalData) ) { this.setState({ formData: this.props.globalData,