From d248361ca7ea62bb6dcbc4e0ebc5e70d126d27b5 Mon Sep 17 00:00:00 2001 From: Prince0906 Date: Sat, 27 Jul 2024 21:25:29 +0530 Subject: [PATCH] Refactored ArrayWidget from class based to functional based --- .../components/manage/Widgets/ArrayWidget.jsx | 504 +++++++++--------- 1 file changed, 246 insertions(+), 258 deletions(-) diff --git a/packages/volto/src/components/manage/Widgets/ArrayWidget.jsx b/packages/volto/src/components/manage/Widgets/ArrayWidget.jsx index 2670cc84bc..2b59ff3778 100644 --- a/packages/volto/src/components/manage/Widgets/ArrayWidget.jsx +++ b/packages/volto/src/components/manage/Widgets/ArrayWidget.jsx @@ -3,10 +3,9 @@ * @module components/manage/Widgets/ArrayWidget */ -import React, { Component } from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import React, { useEffect } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import PropTypes from 'prop-types'; -import { compose } from 'redux'; import { connect } from 'react-redux'; import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; import { find, isObject } from 'lodash'; @@ -125,9 +124,10 @@ const compareOption = (inputValue = '', option, accessors) => { }; /** - * ArrayWidget component class. - * @class ArrayWidget - * @extends Component + * ArrayWidget component. + * @function ArrayWidget + * @param {Object} props Component properties + * @returns {JSX.Element} Markup for the component. * * A creatable select array widget will be rendered if the named vocabulary is * in the widget definition (hint) like: @@ -147,282 +147,270 @@ const compareOption = (inputValue = '', option, accessors) => { * ) * ``` */ -class ArrayWidget 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, - choices: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - ), - vocabLoading: PropTypes.bool, - vocabLoaded: PropTypes.bool, - items: PropTypes.shape({ - vocabulary: PropTypes.object, - }), - widgetOptions: PropTypes.shape({ - vocabulary: PropTypes.object, - }), - value: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - ), - placeholder: PropTypes.string, - onChange: PropTypes.func.isRequired, - wrapped: PropTypes.bool, - creatable: PropTypes.bool, //if widget has no vocab and you want to be creatable - }; - /** - * Default properties - * @property {Object} defaultProps Default properties. - * @static - */ - static defaultProps = { - description: null, - required: false, - items: { - vocabulary: null, - }, - widgetOptions: { - vocabulary: null, - }, - error: [], - choices: [], - value: null, - creatable: false, - }; +const ArrayWidget = ({ + id, + title, + description, + required, + error, + value, + onChange, + choices, + items, + vocabBaseUrl, + vocabLoading, + vocabLoaded, + getVocabulary, + disabled, + isDisabled, + focus, + placeholder, + default: defaultValue, + noValueOption, + reactSelectCreateable, + reactSortableHOC, + reactSelect, + lang, + creatable, +}) => { + const intl = useIntl(); - /** - * Constructor - * @method constructor - * @param {Object} props Component properties - * @constructs Actions - */ - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - } - - /** - * Component did mount - * @method componentDidMount - * @returns {undefined} - */ - componentDidMount() { - if ( - !this.props.items?.choices?.length && - !this.props.choices?.length && - this.props.vocabBaseUrl - ) { - this.props.getVocabulary({ - vocabNameOrURL: this.props.vocabBaseUrl, - size: -1, - subrequest: this.props.lang, - }); - } - } - - componentDidUpdate() { + // Effect to fetch vocabulary if needed + useEffect(() => { if ( - !this.props.items?.choices?.length && - !this.props.choices?.length && - this.props.vocabLoading === undefined && - !this.props.vocabLoaded && - this.props.vocabBaseUrl + !items?.choices?.length && + !choices?.length && + vocabBaseUrl && + vocabLoading === undefined && + !vocabLoaded ) { - this.props.getVocabulary({ - vocabNameOrURL: this.props.vocabBaseUrl, + getVocabulary({ + vocabNameOrURL: vocabBaseUrl, size: -1, - subrequest: this.props.lang, + subrequest: lang, }); } - } + }, [ + items, + choices, + vocabBaseUrl, + vocabLoading, + vocabLoaded, + lang, + getVocabulary, + ]); /** * Handle the field change, store it in the local state and back to simple * array of tokens for correct serialization - * @method handleChange + * @function handleChange * @param {array} selectedOption The selected options (already aggregated). - * @returns {undefined} */ - handleChange(selectedOption) { - this.props.onChange( - this.props.id, + const handleChange = (selectedOption) => { + onChange( + id, selectedOption ? selectedOption.map((item) => item.value) : null, ); - } - - onSortEnd = (selectedOption, { oldIndex, newIndex }) => { - const newValue = arrayMove(selectedOption, oldIndex, newIndex); - - this.handleChange(newValue); }; /** - * Render method. - * @method render - * @returns {string} Markup for the component. + * Handle sorting of selected options + * @function onSortEnd + * @param {array} selectedOption Current selected options + * @param {Object} sortProps Properties from react-sortable-hoc */ - render() { - const choices = normalizeChoices(this.props?.choices || []); - const selectedOption = normalizeArrayValue(choices, this.props.value); - const CreatableSelect = this.props.reactSelectCreateable.default; - const { SortableContainer } = this.props.reactSortableHOC; - const Select = this.props.reactSelect.default; - const SortableSelect = - // It will be only creatable if the named vocabulary is in the widget definition - // (hint) like: - // list_field_voc_unconstrained = schema.List( - // title=u"List field with values from vocabulary but not constrained to them.", - // description=u"zope.schema.List", - // value_type=schema.TextLine(), - // required=False, - // missing_value=[], - // ) - // directives.widget( - // "list_field_voc_unconstrained", - // AjaxSelectFieldWidget, - // vocabulary="plone.app.vocabularies.PortalTypes", - // ) - this.props?.choices && - !getVocabFromHint(this.props) && - !this.props.creatable - ? SortableContainer(Select) - : SortableContainer(CreatableSelect); + const onSortEnd = (selectedOption, { oldIndex, newIndex }) => { + const newValue = arrayMove(selectedOption, oldIndex, newIndex); + handleChange(newValue); + }; + + // Prepare data for rendering + const normalizedChoices = normalizeChoices(choices || []); + const selectedOption = normalizeArrayValue(normalizedChoices, value); + + const CreatableSelect = reactSelectCreateable.default; + const { SortableContainer } = reactSortableHOC; + const Select = reactSelect.default; + const SortableSelect = + // It will be only creatable if the named vocabulary is in the widget definition + // (hint) like: + // list_field_voc_unconstrained = schema.List( + // title=u"List field with values from vocabulary but not constrained to them.", + // description=u"zope.schema.List", + // value_type=schema.TextLine(), + // required=False, + // missing_value=[], + // ) + // directives.widget( + // "list_field_voc_unconstrained", + // AjaxSelectFieldWidget, + // vocabulary="plone.app.vocabularies.PortalTypes", + // ) + choices && !getVocabFromHint({ items }) && !creatable + ? SortableContainer(Select) + : SortableContainer(CreatableSelect); - return ( - - { - this.onSortEnd(selectedOption, sortProp); - }} - menuShouldScrollIntoView={false} - distance={4} - // small fix for https://github.com/clauderic/react-sortable-hoc/pull/352: - getHelperDimensions={({ node }) => node.getBoundingClientRect()} - id={`field-${this.props.id}`} - key={this.props.id} - isDisabled={this.props.disabled || this.props.isDisabled} - className="react-select-container" - classNamePrefix="react-select" - /* eslint-disable jsx-a11y/no-autofocus */ - autoFocus={this.props.focus} - /* eslint-enable jsx-a11y/no-autofocus */ - options={ - this.props.vocabBaseUrl - ? choices - : this.props.choices - ? [ - ...choices, - ...(this.props.noValueOption && - (this.props.default === undefined || - this.props.default === null) - ? [ - { - label: this.props.intl.formatMessage( - messages.no_value, - ), - value: 'no-value', - }, - ] - : []), - ] - : [ - { - label: this.props.intl.formatMessage(messages.no_value), - value: 'no-value', - }, - ] - } - styles={customSelectStyles} - theme={selectTheme} - components={{ - ...(this.props.choices?.length > 25 && { - MenuList, - }), - MultiValueContainer, - MultiValue: SortableMultiValue, - MultiValueLabel: SortableMultiValueLabel, - DropdownIndicator, - ClearIndicator, - Option, - }} - value={selectedOption || []} - placeholder={ - this.props.placeholder ?? - this.props.intl.formatMessage(messages.select) - } - onChange={this.handleChange} - isValidNewOption={( - inputValue, - selectValue, - selectOptions, - accessors, - ) => - !( - !inputValue || - selectValue.some((option) => - compareOption(inputValue, option, accessors), - ) || - selectOptions.some((option) => - compareOption(inputValue, option, accessors), - ) + return ( + + { + onSortEnd(selectedOption, sortProp); + }} + menuShouldScrollIntoView={false} + distance={4} + // small fix for https://github.com/clauderic/react-sortable-hoc/pull/352: + getHelperDimensions={({ node }) => node.getBoundingClientRect()} + id={`field-${id}`} + key={id} + isDisabled={disabled || isDisabled} + className="react-select-container" + classNamePrefix="react-select" + /* eslint-disable jsx-a11y/no-autofocus */ + autoFocus={focus} + /* eslint-enable jsx-a11y/no-autofocus */ + options={ + vocabBaseUrl + ? normalizedChoices + : choices + ? [ + ...normalizedChoices, + ...(noValueOption && + (defaultValue === undefined || defaultValue === null) + ? [ + { + label: intl.formatMessage(messages.no_value), + value: 'no-value', + }, + ] + : []), + ] + : [ + { + label: intl.formatMessage(messages.no_value), + value: 'no-value', + }, + ] + } + styles={customSelectStyles} + theme={selectTheme} + components={{ + ...(choices?.length > 25 && { + MenuList, + }), + MultiValueContainer, + MultiValue: SortableMultiValue, + MultiValueLabel: SortableMultiValueLabel, + DropdownIndicator, + ClearIndicator, + Option, + }} + value={selectedOption || []} + placeholder={placeholder ?? intl.formatMessage(messages.select)} + onChange={handleChange} + isValidNewOption={(inputValue, selectValue, selectOptions, accessors) => + !( + !inputValue || + selectValue.some((option) => + compareOption(inputValue, option, accessors), + ) || + selectOptions.some((option) => + compareOption(inputValue, option, accessors), ) - } - isClearable - isMulti - /> - - ); - } -} + ) + } + isClearable + isMulti + /> + + ); +}; + +ArrayWidget.propTypes = { + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string, + required: PropTypes.bool, + error: PropTypes.arrayOf(PropTypes.string), + getVocabulary: PropTypes.func.isRequired, + choices: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + ), + vocabLoading: PropTypes.bool, + vocabLoaded: PropTypes.bool, + items: PropTypes.shape({ + vocabulary: PropTypes.object, + }), + widgetOptions: PropTypes.shape({ + vocabulary: PropTypes.object, + }), + value: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + ), + placeholder: PropTypes.string, + onChange: PropTypes.func.isRequired, + wrapped: PropTypes.bool, + creatable: PropTypes.bool, //if widget has no vocab and you want to be creatable +}; + +ArrayWidget.defaultProps = { + description: null, + required: false, + items: { + vocabulary: null, + }, + widgetOptions: { + vocabulary: null, + }, + error: [], + choices: [], + value: null, + creatable: false, +}; -export const ArrayWidgetComponent = injectIntl(ArrayWidget); +export const ArrayWidgetComponent = ArrayWidget; -export default compose( - injectIntl, - injectLazyLibs(['reactSelect', 'reactSelectCreateable', 'reactSortableHOC']), - connect( - (state, props) => { - const vocabBaseUrl = - getVocabFromHint(props) || - getVocabFromField(props) || - getVocabFromItems(props); +export default connect( + (state, props) => { + const vocabBaseUrl = + getVocabFromHint(props) || + getVocabFromField(props) || + getVocabFromItems(props); - const vocabState = - state.vocabularies?.[vocabBaseUrl]?.subrequests?.[state.intl.locale]; + 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.items?.choices) { - return { - choices: props.items.choices, - lang: state.intl.locale, - }; - } else if (vocabState) { - return { - choices: vocabState.items, - vocabBaseUrl, - vocabLoading: vocabState.loading, - vocabLoaded: vocabState.loaded, - lang: state.intl.locale, - }; - } - return { vocabBaseUrl, lang: state.intl.locale }; - }, - { getVocabulary }, + // If the schema already has the choices in it, then do not try to get the vocab, + // even if there is one + if (props.items?.choices) { + return { + choices: props.items.choices, + lang: state.intl.locale, + }; + } else if (vocabState) { + return { + choices: vocabState.items, + vocabBaseUrl, + vocabLoading: vocabState.loading, + vocabLoaded: vocabState.loaded, + lang: state.intl.locale, + }; + } + return { vocabBaseUrl, lang: state.intl.locale }; + }, + { getVocabulary }, +)( + injectLazyLibs(['reactSelect', 'reactSelectCreateable', 'reactSortableHOC'])( + ArrayWidget, ), -)(ArrayWidget); +);