diff --git a/sass/widgets/common/WidgetForm.scss b/sass/widgets/common/WidgetForm.scss index f8030481d..60dc88b85 100644 --- a/sass/widgets/common/WidgetForm.scss +++ b/sass/widgets/common/WidgetForm.scss @@ -2,7 +2,6 @@ .WidgetForm { &__body { display: flex; - height: 100%; } &__info { diff --git a/sass/widgets/common/WidgetPage.scss b/sass/widgets/common/WidgetPage.scss index e91b4b906..2e8ff1f2a 100644 --- a/sass/widgets/common/WidgetPage.scss +++ b/sass/widgets/common/WidgetPage.scss @@ -1,7 +1,6 @@ // sass-lint:disable class-name-format no-important .WidgetPage { - height: 100%; - min-height: 700px; + min-height: 100%; margin-bottom: 0; background-color: $color-grey-background; diff --git a/src/ui/common/formik-field/FilterValueOptionSelector.js b/src/ui/common/formik-field/FilterValueOptionSelector.js new file mode 100644 index 000000000..fa6033bf2 --- /dev/null +++ b/src/ui/common/formik-field/FilterValueOptionSelector.js @@ -0,0 +1,653 @@ +import React, { Fragment, Component } from 'react'; +import PropTypes from 'prop-types'; +import { get, isUndefined, isEqual } from 'lodash'; +import { FormattedMessage, FormattedHTMLMessage, intlShape } from 'react-intl'; +import { Collapse } from 'react-collapse'; +import { Field } from 'formik'; +import FormLabel from 'ui/common/form/FormLabel'; +import RadioInput from 'ui/common/formik-field/RenderRadioInput'; +import TextInput from 'ui/common/formik-field/RenderTextInput'; +import DateFilterInput from 'ui/common/form/RenderDateFilterInput'; +import SwitchRenderer from 'ui/common/formik-field/RenderSwitchInput'; + +import { + TEXT_FILTERABLE_ATTRIBUTES, + DATE_FILTERABLE_ATTRIBUTES, + BOOL_FILTERABLE_ATTRIBUTES, +} from 'state/content-type/selectors'; + +const TEXT_FILTERABLE = 'text_filterable_type'; +const DATE_FILTERABLE = 'date_filterable_type'; +const BOOL_FILTERABLE = 'boolean_filterable_type'; + +const HAS_VALUE = 'valuePresence'; +const HAS_NO_VALUE = 'valueAbsence'; +const BY_VALUE_ONLY = 'valueOnly'; +const BY_VALUE_PARTIAL = 'valuePartial'; +const BY_RANGE = 'valueRange'; + +const CLEAN_VALUES = { + value: undefined, + nullValue: undefined, + likeOption: undefined, + valueDateDelay: undefined, + startDateDelay: undefined, + endDateDelay: undefined, + start: undefined, + end: undefined, +}; + +class FilterValueOptionSelector extends Component { + constructor(props) { + super(props); + this.state = { + filterableType: '', + expanded: false, + optionSelected: '', + keyChanged: false, + }; + this.handleHeadlineClick = this.handleHeadlineClick.bind(this); + this.handleValueChange = this.handleValueChange.bind(this); + this.handleValueTypeChange = this.handleValueTypeChange.bind(this); + } + + componentDidMount() { + this.beginFillState(); + } + + componentDidUpdate(prevProps) { + const { + attributeFilterChoices: prevAttr, + value: prevValue, + } = prevProps; + const { key: prevKey } = prevValue; + + const { + attributeFilterChoices: attr, + value, + } = this.props; + const { key } = value; + + const hasAllValueChanged = !isEqual({ ...prevValue, key: 1 }, { ...value, key: 1 }); + + const keyChanged = prevKey !== key; + if (keyChanged || prevAttr.length !== attr.length) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ keyChanged }); + this.beginFillState(hasAllValueChanged); + } + } + + componentWillUnmount() { + this.setState({ + filterableType: '', + expanded: false, + optionSelected: '', + }); + } + + getAttributeType() { + const { value } = this.props; + const { key } = value; + const { attributeFilterChoices } = this.props; + const selectedAttributeType = attributeFilterChoices.find(attribute => attribute.code === key); + return get(selectedAttributeType, 'type', ''); + } + + beginFillState(valuePropChanged) { + const { + attributeFilterChoices: attrChoices, + value: { attributeFilter }, + } = this.props; + const { keyChanged } = this.state; + + if (attributeFilter && attrChoices && attrChoices.length > 0) { + const filterableType = this.determineFilterableType(); + if (keyChanged && !valuePropChanged) { + if (filterableType === BOOL_FILTERABLE) { + this.handleValueChange({ ...CLEAN_VALUES, value: true }); + } else { + this.handleValueChange({ ...CLEAN_VALUES }); + } + } + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + filterableType, + optionSelected: this.determineOptionSelected(filterableType), + }); + } + } + + determineFilterableType() { + const type = this.getAttributeType(); + if (TEXT_FILTERABLE_ATTRIBUTES.includes(type)) { + return TEXT_FILTERABLE; + } + if (DATE_FILTERABLE_ATTRIBUTES.includes(type)) { + return DATE_FILTERABLE; + } + if (BOOL_FILTERABLE_ATTRIBUTES.includes(type)) { + return BOOL_FILTERABLE; + } + return ''; + } + + determineOptionSelected(filterableType) { + const { value: propValue } = this.props; + const { + value, + start, + end, + nullValue, + } = propValue; + if (!isUndefined(start) || !isUndefined(end)) { + return BY_RANGE; + } + if (value) { + return filterableType === TEXT_FILTERABLE ? BY_VALUE_PARTIAL : BY_VALUE_ONLY; + } + if (nullValue) { + return HAS_NO_VALUE; + } + return HAS_VALUE; + } + + handleHeadlineClick() { + const { expanded } = this.state; + this.setState({ expanded: !expanded }); + } + + handleValueChange(newProp) { + const { value: propValue, onChange, fieldIndex } = this.props; + const newValue = { + ...propValue, + ...newProp, + }; + onChange(newValue, fieldIndex); + } + + resetValueKeys(option, filterableType) { + switch (option) { + case BY_VALUE_ONLY: + if (filterableType === DATE_FILTERABLE) { + this.handleValueChange({ ...CLEAN_VALUES, value: 'today' }); + } else { + this.handleValueChange({ ...CLEAN_VALUES, value: '' }); + } + break; + case BY_VALUE_PARTIAL: + this.handleValueChange({ ...CLEAN_VALUES, value: '', likeOption: false }); + break; + case BY_RANGE: + this.handleValueChange({ ...CLEAN_VALUES, start: '', end: '' }); + break; + case HAS_NO_VALUE: + this.handleValueChange({ ...CLEAN_VALUES, nullValue: true }); + break; + case HAS_VALUE: + default: + this.handleValueChange({ ...CLEAN_VALUES }); + } + } + + handleValueTypeChange({ currentTarget: { value } }) { + const { filterableType } = this.state; + this.resetValueKeys(value, filterableType); + this.setState({ optionSelected: value }); + } + + renderLabelWithSort(node) { + const { value } = this.props; + const { order } = value; + return ( + + {node}{order && ( + + )} + + ); + } + + renderLabelValue() { + const { value, intl } = this.props; + const { key, attributeFilter } = value; + const { optionSelected, filterableType } = this.state; + if (!attributeFilter) { + return this.renderLabelWithSort(); + } + const { + value: filterValue, + start, + end, + valueDateDelay, + startDateDelay, + endDateDelay, + likeOption, + } = value; + + if (filterableType === BOOL_FILTERABLE) { + if (!isUndefined(filterValue)) { + return this.renderLabelWithSort(); + } + return this.renderLabelWithSort(); + } + + switch (optionSelected) { + case BY_VALUE_ONLY: { + const rendered = ( + + + {valueDateDelay && ( + + )} + + ); + return this.renderLabelWithSort(rendered); + } + case BY_VALUE_PARTIAL: { + const partial = likeOption + && intl.formatMessage({ id: 'widget.form.filterable.valuePartialPhrase' }); + return this.renderLabelWithSort(); + } + case BY_RANGE: { + const fromMsg = start && ( + + + {startDateDelay && ( + + )} + + ); + const toMsg = end && ( + + + {endDateDelay && ( + + )} + + ); + + const rendered = ( + + + {' '} + {fromMsg} + {' '} + {toMsg} + + ); + + return this.renderLabelWithSort(rendered); + } + case HAS_NO_VALUE: + return this.renderLabelWithSort(); + default: + return this.renderLabelWithSort(); + } + } + + renderSelectOptions(options, keyCode = 'value') { + const { intl } = this.props; + return options + .map(item => ( + + )); + } + + renderBooleanFilterValueOptions() { + const { intl, value: propValue, filter } = this.props; + const { value: filterValue } = propValue; + const boolChoices = [ + { + id: 'true', + label: intl.formatMessage({ id: 'cms.label.yes' }), + }, + { + id: 'false', + label: intl.formatMessage({ id: 'cms.label.no' }), + }, + { + id: 'all', + label: intl.formatMessage({ id: 'cms.label.all' }), + }, + ]; + const boolValue = filterValue ? 'true' : 'false'; + const value = isUndefined(filterValue) ? 'all' : boolValue; + + const input = { + name: `${filter}.value`, + type: 'radio', + value, + onChange: (valueSelected) => { + switch (valueSelected) { + case 'true': + this.handleValueChange({ value: true }); + break; + case 'false': + this.handleValueChange({ value: false }); + break; + case 'all': + default: + this.handleValueChange({ value: undefined }); + break; + } + }, + }; + + return ( + +
+ +
+ ); + } + + renderValueOnlyFields() { + const { value: propValue, filter } = this.props; + const { value } = propValue; + const { filterableType } = this.state; + const input = { + name: `${filter}.value`, + value, + }; + return ( + filterableType !== DATE_FILTERABLE ? ( +
+ ( + this.handleValueChange({ value: v }) + ), + }} + label={} + /> +
+ ) : ( +
+ this.handleValueChange(values), + }} + delayKey="valueDateDelay" + label={} + /> +
+ ) + ); + } + + renderRangeFields() { + const { value: propValue, filter } = this.props; + const { start, end } = propValue; + const { filterableType } = this.state; + + if (filterableType === TEXT_FILTERABLE) { + const handleChangeRange = forValue => ({ target: { value } }) => ( + this.handleValueChange({ [forValue]: value }) + ); + + return ( +
+ } + /> + } + /> +
+ ); + } + const handleChangeDateRange = forValue => values => ( + this.handleValueChange({ + ...values, + [forValue]: values.value, + value: undefined, + }) + ); + return ( +
+ } + /> + } + /> +
+ ); + } + + renderFilterValueOptions() { + const { filterableType, optionSelected } = this.state; + const { value: propValue, filter } = this.props; + const { value, likeOption } = propValue; + const filterChoices = [ + { value: HAS_VALUE, nameId: 'widget.form.filterable.labelPresence' }, + { value: HAS_NO_VALUE, nameId: 'widget.form.filterable.labelAbsence' }, + ( + filterableType === TEXT_FILTERABLE ? ( + { value: BY_VALUE_PARTIAL, nameId: 'widget.form.filterable.labelPartial' } + ) : ( + { value: BY_VALUE_ONLY, nameId: 'widget.form.filterable.labelOnly' } + ) + ), + { value: BY_RANGE, nameId: 'widget.form.filterable.labelRange' }, + ]; + + return ( + +
+ + {optionSelected === BY_VALUE_ONLY && ( + this.renderValueOnlyFields() + )} + {optionSelected === BY_VALUE_PARTIAL && ( +
+ ( + this.handleValueChange({ value: v }) + ), + }} + labelSize={3} + label={} + /> + this.handleValueChange({ likeOption: like || undefined }), + }} + label={} + labelSize={3} + switchVals={{ + trueValue: 'true', + falseValue: 'false', + }} + /> +
+ )} + {optionSelected === BY_RANGE && this.renderRangeFields()} +
+ ); + } + + renderAttributeFilter() { + const { filterableType } = this.state; + if (filterableType === BOOL_FILTERABLE) { + return this.renderBooleanFilterValueOptions(); + } + return this.renderFilterValueOptions(); + } + + render() { + const { expanded } = this.state; + const { value: propValue, intl, filter } = this.props; + const { + order, + attributeFilter, + key, + } = propValue; + + const sortFilters = [ + { + id: '', + label: intl.formatMessage({ id: 'app.enumerator.none' }), + }, + { + id: 'ASC', + label: intl.formatMessage({ id: 'widget.form.asc' }), + }, + { + id: 'DESC', + label: intl.formatMessage({ id: 'widget.form.desc' }), + }, + ]; + + if (!key) return null; + + return ( + + +
+
+ {this.renderLabelValue()} +
+ +
+
+ +
+ {attributeFilter && this.renderAttributeFilter()} +
+ this.handleValueChange({ order: ord })} + hasLabel={false} + toggleElement={sortFilters} + meta={{ touched: false, error: false }} + /> +
+
+
+ ); + } +} + +FilterValueOptionSelector.propTypes = { + intl: intlShape.isRequired, + value: PropTypes.shape({ + value: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + start: PropTypes.string, + end: PropTypes.string, + nullValue: PropTypes.bool, + key: PropTypes.string, + attributeFilter: PropTypes.bool, + likeOption: PropTypes.bool, + order: PropTypes.string, + valueDateDelay: PropTypes.string, + startDateDelay: PropTypes.string, + endDateDelay: PropTypes.string, + }), + filter: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + fieldIndex: PropTypes.number.isRequired, + attributeFilterChoices: PropTypes.arrayOf(PropTypes.shape({})), +}; + +FilterValueOptionSelector.defaultProps = { + value: {}, + attributeFilterChoices: [], +}; + +export default FilterValueOptionSelector; diff --git a/src/ui/common/formik-field/FiltersSelectRenderer.js b/src/ui/common/formik-field/FiltersSelectRenderer.js new file mode 100644 index 000000000..3562b7eed --- /dev/null +++ b/src/ui/common/formik-field/FiltersSelectRenderer.js @@ -0,0 +1,181 @@ +import React from 'react'; +import { Field, getIn } from 'formik'; +import PropTypes from 'prop-types'; +import { Table } from 'react-bootstrap'; +import { FormattedMessage, intlShape } from 'react-intl'; +import { Button, ButtonGroup } from 'patternfly-react'; + +import FilterValueOptionSelector from 'ui/common/formik-field/FilterValueOptionSelector'; + +const FiltersSelectRenderer = (props) => { + const { + filterName, attributeFilterChoices, + options, suboptions, intl, + onChangeFilterValue, onChangeFilterAttribute, + onResetFilterOption, form, push, swap, remove, name, + } = props; + + const filterOptions = opts => opts + .map(item => ( + + )); + + const handleAddNewFilter = () => push({}); + + const handleFilterAttributeChange = (value, index) => { + onResetFilterOption(filterName, index, value); + const attributeFilter = attributeFilterChoices.findIndex(({ code }) => code === value) > -1; + onChangeFilterAttribute(name, index, attributeFilter); + }; + + const handleFilterChange = (value, index) => ( + onChangeFilterValue(name, index, value) + ); + + const renderFilters = getIn(form.values, name) && getIn(form.values, name).map((filter, i) => { + const { key } = filter; + return ( + // eslint-disable-next-line react/no-array-index-key + + +
remove(i)} + onKeyDown={() => remove(i)} + tabIndex={0} + role="button" + > + +
+ + + handleFilterAttributeChange(currentTarget.value, i) + } + > + {filterOptions(options)} + + + + {(filterName === 'filters' || filterName === 'config.filters') && ( + + )} + {filterName !== 'filters' && key + && suboptions[key] && suboptions[key].length > 0 && ( + + {filterOptions(suboptions[key])} + + )} + + + + { + i !== 0 + && ( + + ) + } + { + i !== getIn(form.values, name).length - 1 + && ( + + ) + } + + + + ); + }); + + + return ( +
+ + + + + + + + + + + {renderFilters} + +
+ + + + + + + +
+
+ +
+
+
+ ); +}; + + +FiltersSelectRenderer.propTypes = { + intl: intlShape.isRequired, + form: PropTypes.shape({ + initialValues: PropTypes.shape({}), + values: PropTypes.shape({}), + }).isRequired, + push: PropTypes.func.isRequired, + swap: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + suboptions: PropTypes.shape({}), + onChangeFilterAttribute: PropTypes.func.isRequired, + onChangeFilterValue: PropTypes.func.isRequired, + onResetFilterOption: PropTypes.func.isRequired, + filterName: PropTypes.string.isRequired, + attributeFilterChoices: PropTypes.arrayOf(PropTypes.shape({})).isRequired, +}; + +FiltersSelectRenderer.defaultProps = { + suboptions: {}, +}; + +export default FiltersSelectRenderer; diff --git a/src/ui/common/formik-field/MultiFilterSelectRenderer.js b/src/ui/common/formik-field/MultiFilterSelectRenderer.js new file mode 100644 index 000000000..7cdefe1cc --- /dev/null +++ b/src/ui/common/formik-field/MultiFilterSelectRenderer.js @@ -0,0 +1,129 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { formattedText } from '@entando/utils'; +import { InputGroup, Button, Label } from 'patternfly-react'; + + +class MultiFilterSelectRenderer extends Component { + constructor(props) { + super(props); + this.pushField = this.pushField.bind(this); + this.renderTags = this.renderTags.bind(this); + this.select = null; + } + + pushField() { + if (!this.select || !this.select.value) { + return; + } + const { + selectedValues, allMode, + push, form: { setFieldValue }, name, + } = this.props; + + const allBool = allMode && this.select.value === 'all'; + + if (allBool) { + setFieldValue(name, []); + } + + if (this.select.value && !selectedValues.includes(this.select.value) && !allBool) { + push(this.select.value); + } + } + + renderTags() { + const { + selectedValues, labelKey, valueKey, options, + remove, + } = this.props; + return selectedValues && selectedValues.map((value, i) => { + const elem = options.length ? options.find(opt => opt[valueKey] === value) : null; + return ( + + ); + }); + } + + render() { + const { + options, selectedValues, labelKey, valueKey, emptyOptionTextId, allMode, + } = this.props; + + const filteredOptions = options + .filter(opt => !selectedValues.includes(opt[valueKey]) || allMode) + .map(item => ( + + )); + + if (emptyOptionTextId) { + const emptyOptionText = formattedText(emptyOptionTextId); + filteredOptions.unshift(( + + )); + } + + return ( +
+ + + + + + +
+ { this.renderTags() } +
+ ); + } +} + + +MultiFilterSelectRenderer.propTypes = { + name: PropTypes.string.isRequired, + form: PropTypes.shape({ + setFieldValue: PropTypes.func, + }).isRequired, + push: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + selectedValues: PropTypes.arrayOf(PropTypes.string), + valueKey: PropTypes.string, + labelKey: PropTypes.string, + emptyOptionTextId: PropTypes.string, + allMode: PropTypes.bool, +}; + +MultiFilterSelectRenderer.defaultProps = { + selectedValues: [], + valueKey: 'value', + labelKey: 'label', + emptyOptionTextId: '', + allMode: false, +}; + +export default MultiFilterSelectRenderer; diff --git a/src/ui/common/formik-field/SelectInput.js b/src/ui/common/formik-field/SelectInput.js index a7d773deb..5fe6a05ab 100644 --- a/src/ui/common/formik-field/SelectInput.js +++ b/src/ui/common/formik-field/SelectInput.js @@ -24,6 +24,7 @@ const SelectInput = ({ intl, hasLabel, xsClass, + onChange, }) => { const { touched, error } = getTouchErrorByField(field.name, form); @@ -74,6 +75,12 @@ const SelectInput = ({ className="form-control SelectInput" disabled={disabled} ref={forwardedRef} + onChange={(e) => { + field.onChange(e); + if (onChange) { + onChange(e); + } + }} > {defaultOption} {optionsList} @@ -88,6 +95,7 @@ SelectInput.propTypes = { intl: intlShape.isRequired, field: PropTypes.shape({ name: PropTypes.string.isRequired, + onChange: PropTypes.func, }).isRequired, form: PropTypes.shape({ touched: PropTypes.shape({}), @@ -114,6 +122,8 @@ SelectInput.propTypes = { inputSize: PropTypes.number, disabled: PropTypes.bool, hasLabel: PropTypes.bool, + onChange: PropTypes.func, + }; SelectInput.defaultProps = { @@ -133,6 +143,7 @@ SelectInput.defaultProps = { disabled: false, hasLabel: true, forwardedRef: null, + onChange: null, }; const IntlWrappedSelectInput = injectIntl(SelectInput); diff --git a/src/ui/widget-forms/ContentsQueryConfig.js b/src/ui/widget-forms/ContentsQueryConfig.js index ca621eb30..b1e6984ea 100644 --- a/src/ui/widget-forms/ContentsQueryConfig.js +++ b/src/ui/widget-forms/ContentsQueryConfig.js @@ -1,23 +1,24 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; +import { addToast, TOAST_SUCCESS } from '@entando/messages'; +import { ROUTE_APP_BUILDER_PAGE_CONFIG } from 'app-init/router'; import { FormattedMessage, intlShape } from 'react-intl'; -import { Field, FieldArray, reduxForm } from 'redux-form'; +import { Field, FieldArray, withFormik, getIn } from 'formik'; import { Collapse } from 'react-collapse'; import { Button, Row, Col, FormGroup, Alert } from 'patternfly-react'; -import { required, maxLength } from '@entando/utils'; +import { routeConverter, required, maxLength } from '@entando/utils'; import { isUndefined } from 'lodash'; - -import RenderTextInput from 'ui/common/form/RenderTextInput'; -import RenderSelectInput from 'ui/common/form/RenderSelectInput'; +import RenderTextInput from 'ui/common/formik-field/RenderTextInput'; +import RenderSelectInput from 'ui/common/formik-field/SelectInput'; import FormLabel from 'ui/common/form/FormLabel'; import SectionTitle from 'ui/common/SectionTitle'; -import MultiFilterSelectRenderer from 'ui/common/form/MultiFilterSelectRenderer'; -import FiltersSelectRenderer from 'ui/common/form/FiltersSelectRenderer'; +import MultiFilterSelectRenderer from 'ui/common/formik-field/MultiFilterSelectRenderer'; +import FiltersSelectRenderer from 'ui/common/formik-field/FiltersSelectRenderer'; import ConfirmCancelModalContainer from 'ui/common/cancel-modal/ConfirmCancelModalContainer'; import NoDefaultWarningModal from 'ui/widget-forms/publish-single-content-config/NoDefaultWarningModal'; - import { CONTENTS_QUERY_CONFIG } from 'ui/widget-forms/const'; import WidgetConfigPortal from 'ui/widgets/config/WidgetConfigPortal'; +import { convertReduxValidationsToFormikValidations } from 'helpers/formikUtils'; export const ContentsQueryContainerId = `widgets.${CONTENTS_QUERY_CONFIG}`; @@ -53,10 +54,18 @@ export class ContentsQueryFormBody extends Component { } componentDidUpdate(prevProps) { - const { selectedContentType: prevContentType } = prevProps; - const { selectedContentType, onChangeContentType } = this.props; - if (selectedContentType !== prevContentType) { - onChangeContentType(selectedContentType); + const { values: prevValues } = prevProps; + const { + onChangeContentType, values: currentValues, setFieldValue, putPrefixField, + contentType, initialValues, setSelectedContentType, contentTypes, + } = this.props; + if (initialValues.contentType !== '' && !contentType.code) { + const selectedContentTypeFromForm = contentTypes + .find(ctype => ctype.code === initialValues.contentType); + if (selectedContentTypeFromForm) setSelectedContentType(selectedContentTypeFromForm); + } + if (getIn(prevValues, putPrefixField('contentType')) !== getIn(currentValues, putPrefixField('contentType'))) { + onChangeContentType(getIn(currentValues, putPrefixField('contentType')), setFieldValue); } } @@ -81,11 +90,12 @@ export class ContentsQueryFormBody extends Component { renderFormFields() { const { contentTypes, contentType, contentTemplates, categories, pages, - onResetModelId, selectedContentType, selectedCategories, + onResetModelId, intl, onChangeFilterValue, onResetFilterOption, onChangeFilterAttribute, - languages, onToggleInclusiveOr, selectedInclusiveOr, extFormName, - invalid, submitting, dirty, onCancel, onDiscard, onSave, putPrefixField, - widgetConfigFormData, defaultLanguageCode, + languages, onToggleInclusiveOr, extFormName, + isValid, isDirty, isSubmitting, onCancel, onDiscard, putPrefixField, + widgetConfigFormData, defaultLanguageCode, values, setFieldValue, handleSubmit, + onSave, submitForm, } = this.props; const { publishingSettings, filters: filtersPanel, @@ -99,7 +109,7 @@ export class ContentsQueryFormBody extends Component { const normalizedLanguages = languages.map(lang => lang.code); - const defaultPageValue = widgetConfigFormData[putPrefixField('pageLink')]; + const defaultPageValue = getIn(values, putPrefixField('pageLink')); const defaultLangLinkTextRequired = defaultPageValue !== null && defaultPageValue !== undefined && defaultPageValue !== ''; const renderTitleFields = !isUndefined(normalizedLanguages) ? normalizedLanguages @@ -109,7 +119,7 @@ export class ContentsQueryFormBody extends Component { component={RenderTextInput} name={putPrefixField(`title_${langCode}`)} label={} - validate={[maxLength70]} + validate={value => convertReduxValidationsToFormikValidations(value, [maxLength70])} /> )) : null; @@ -125,9 +135,15 @@ export class ContentsQueryFormBody extends Component { labelId="widget.form.linkText" required={langCode === defaultLanguageCode && defaultLangLinkTextRequired} /> -)} - validate={langCode === defaultLanguageCode && defaultLangLinkTextRequired - ? [required, maxLength70] : [maxLength70]} + )} + validate={value => + convertReduxValidationsToFormikValidations( + value, + langCode === defaultLanguageCode + && defaultLangLinkTextRequired + ? [required, maxLength70] : [maxLength70], + ) + } /> )) : null; @@ -139,7 +155,7 @@ export class ContentsQueryFormBody extends Component { const inclusiveOrOptions = [{ id: 'true', label: intl.formatMessage({ id: 'widget.form.inclusiveOr' }) }]; - const renderCategories = selectedCategories; + const renderCategories = getIn(values, putPrefixField('categories')); const getListAttributeFilters = () => { if (!contentType.attributes) { @@ -157,11 +173,15 @@ export class ContentsQueryFormBody extends Component { @@ -211,13 +231,13 @@ export class ContentsQueryFormBody extends Component { ...attributeFilters.map(({ code }) => ({ [code]: [] })), }; - const handleContentTypeChange = () => onResetModelId(); + const handleContentTypeChange = () => onResetModelId(setFieldValue); const handleCollapsePublishingSettings = () => this.collapseSection('publishingSettings'); const handleCollapseFilters = () => this.collapseSection('filters'); const handleCollapseExtraOptions = () => this.collapseSection('extraOptions'); const handleCollapseFrontendFilters = () => this.collapseSection('frontendFilters'); const handleCancelClick = () => { - if (dirty) { + if (isDirty) { onCancel(); } else { onDiscard(); @@ -234,21 +254,23 @@ export class ContentsQueryFormBody extends Component { noRequired /> - } + } options={contentTypes} optionValue="code" optionDisplayName="name" defaultOptionId="app.enumerator.none" - onChange={handleContentTypeChange} + component={RenderSelectInput} + onChange={() => { + handleContentTypeChange(); + }} /> -
+
@@ -320,19 +342,22 @@ export class ContentsQueryFormBody extends Component { ()} + />
@@ -347,15 +372,25 @@ export class ContentsQueryFormBody extends Component { ( onResetFilterOption(name, i, value, setFieldValue) + } + onChangeFilterValue={ + (name, index, value) => + onChangeFilterValue(name, index, value, setFieldValue) + } + onChangeFilterAttribute={ + (name, index, attributeFilter) => + onChangeFilterAttribute(name, index, attributeFilter, setFieldValue) + } + filterName={putPrefixField('filters')} + attributeFilterChoices={attributeFilters} + />)} /> @@ -386,8 +421,13 @@ export class ContentsQueryFormBody extends Component { name={putPrefixField('pageLink')} label={ - } - validate={pageIsRequired ? [required] : []} + } + validate={value => + convertReduxValidationsToFormikValidations( + value, + pageIsRequired ? [required] : [], + ) + } options={normalizedPages} optionValue="code" optionDisplayName="name" @@ -418,16 +458,27 @@ export class ContentsQueryFormBody extends Component { ( onResetFilterOption(name, i, value, setFieldValue) + } + onChangeFilterValue={ + (name, index, value) => + onChangeFilterValue(name, index, value, setFieldValue) + } + onChangeFilterAttribute={ + (name, index, attributeFilter) => + onChangeFilterAttribute(name, index, attributeFilter, setFieldValue) + } + filterName={putPrefixField('userFilters')} + attributeFilterChoices={attributeFilters} + />)} + /> @@ -452,9 +503,9 @@ export class ContentsQueryFormBody extends Component { { onSave(submitForm); handleSubmit(); }} onDiscard={onDiscard} /> @@ -478,8 +529,10 @@ export class ContentsQueryFormBody extends Component { ); } + render() { const { extFormName } = this.props; + const formFields = this.renderFormFields(); return ( @@ -500,11 +553,12 @@ ContentsQueryFormBody.propTypes = { languages: PropTypes.arrayOf(PropTypes.shape({})), onDidMount: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, - invalid: PropTypes.bool, - submitting: PropTypes.bool, + submitForm: PropTypes.func.isRequired, + setSelectedContentType: PropTypes.func.isRequired, contentTypes: PropTypes.arrayOf(PropTypes.shape({})), contentType: PropTypes.shape({ attributes: PropTypes.arrayOf(PropTypes.shape({})), + code: PropTypes.string, }), contentTemplates: PropTypes.arrayOf(PropTypes.shape({})), pages: PropTypes.arrayOf(PropTypes.shape({})), @@ -516,9 +570,10 @@ ContentsQueryFormBody.propTypes = { onChangeContentType: PropTypes.func.isRequired, onToggleInclusiveOr: PropTypes.func.isRequired, onResetModelId: PropTypes.func.isRequired, - selectedContentType: PropTypes.string, - selectedInclusiveOr: PropTypes.string, - dirty: PropTypes.bool, + setFieldValue: PropTypes.func.isRequired, + isValid: PropTypes.bool, + isDirty: PropTypes.bool, + isSubmitting: PropTypes.bool, onDiscard: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired, @@ -527,11 +582,20 @@ ContentsQueryFormBody.propTypes = { cloneMode: PropTypes.bool, widgetConfigFormData: PropTypes.shape({}), defaultLanguageCode: PropTypes.string, + values: PropTypes.shape({ + contentType: PropTypes.string, + categories: PropTypes.arrayOf(PropTypes.string), + orClauseCategoryFilter: PropTypes.string, + }), + initialValues: PropTypes.shape({ + contentType: PropTypes.string, + }).isRequired, }; ContentsQueryFormBody.defaultProps = { - invalid: false, - submitting: false, + isValid: false, + isDirty: false, + isSubmitting: false, languages: [], contentTypes: [], contentType: {}, @@ -539,18 +603,55 @@ ContentsQueryFormBody.defaultProps = { categories: [], pages: [], selectedCategories: [], - selectedContentType: '', - selectedInclusiveOr: '', - dirty: false, extFormName: '', putPrefixField: name => name, cloneMode: false, widgetConfigFormData: {}, defaultLanguageCode: 'en', + values: { + contentType: '', + categories: [], + orClauseCategoryFilter: '', + }, }; -const ContentsQueryConfig = reduxForm({ - form: ContentsQueryContainerId, +const ContentsQueryConfig = withFormik({ + enableReinitialize: true, + mapPropsToValues: ({ initialValues, languages, parentField }) => { + let formValues = { + ...initialValues, + ...languages.reduce((acc, item) => ({ + ...acc, + [`title_${item.code}`]: initialValues[`title_${item.code}`] || '', + [`linkDescr_${item.code}`]: initialValues[`linkDescr_${item.code}`] || '', + }), {}), + }; + if (parentField) { + formValues = { + [parentField]: formValues, + }; + } + + return formValues; + }, + handleSubmit(values, { + setSubmitting, + props: { + onSubmit, history, intl, continueWithDispatch, pageCode, + }, + }) { + onSubmit(values) + .then((res) => { + if (res) { + continueWithDispatch(addToast( + intl.formatMessage({ id: 'widget.update.success' }), + TOAST_SUCCESS, + )); + history.push(routeConverter(ROUTE_APP_BUILDER_PAGE_CONFIG, { pageCode })); + } + }) + .finally(() => setSubmitting(false)); + }, })(ContentsQueryFormBody); export default ContentsQueryConfig; diff --git a/src/ui/widget-forms/ContentsQueryConfigContainer.js b/src/ui/widget-forms/ContentsQueryConfigContainer.js index b641452ea..6a8845867 100644 --- a/src/ui/widget-forms/ContentsQueryConfigContainer.js +++ b/src/ui/widget-forms/ContentsQueryConfigContainer.js @@ -1,8 +1,7 @@ import { connect } from 'react-redux'; -import { clearErrors, addToast, TOAST_SUCCESS } from '@entando/messages'; +import { clearErrors } from '@entando/messages'; import { get, isUndefined, isNull } from 'lodash'; import { injectIntl } from 'react-intl'; -import { change, formValueSelector, submit, getFormValues } from 'redux-form'; import { routeConverter } from '@entando/utils'; import { ROUTE_APP_BUILDER_PAGE_CONFIG } from 'app-init/router'; @@ -10,12 +9,12 @@ import { sendPutWidgetConfig } from 'state/page-config/actions'; import { fetchSearchPages } from 'state/pages/actions'; import { fetchLanguages } from 'state/languages/actions'; import { fetchCategoryTreeAll } from 'state/categories/actions'; -import { fetchContentTypeListPaged, fetchContentType } from 'state/content-type/actions'; +import { fetchContentTypeListPaged, fetchContentType, setSelectedContentType } from 'state/content-type/actions'; import { fetchContentTemplatesByContentType } from 'state/content-template/actions'; import { getContentTypeList, getSelectedContentType } from 'state/content-type/selectors'; import { getCategoryTree } from 'state/categories/selectors'; -import ContentsQueryConfig, { ContentsQueryContainerId, ContentsQueryFormBody } from 'ui/widget-forms/ContentsQueryConfig'; +import ContentsQueryConfig, { ContentsQueryFormBody } from 'ui/widget-forms/ContentsQueryConfig'; import { getContentTemplateList } from 'state/content-template/selectors'; import { getLocale } from 'state/locale/selectors'; import { getSearchPagesRaw } from 'state/pages/selectors'; @@ -27,11 +26,23 @@ import { NoDefaultWarningModalId } from 'ui/widget-forms/publish-single-content- const nopage = { page: 1, pageSize: 0 }; export const mapStateToProps = (state, ownProps) => { - const formToUse = get(ownProps, 'extFormName', ContentsQueryContainerId); - const parentField = get(ownProps, 'input.name', ''); + const parentField = get(ownProps, 'field.name', ''); const putPrefixField = field => (parentField !== '' ? `${parentField}.${field}` : field); + + const INITIAL_VALUES = { + [putPrefixField('contentType')]: '', + [putPrefixField('modelId')]: '', + [putPrefixField('maxElemForItem')]: '', + [putPrefixField('maxElements')]: '', + [putPrefixField('categories')]: [], + [putPrefixField('filters')]: [], + [putPrefixField('pageLink')]: '', + [putPrefixField('userFilters')]: [], + [putPrefixField('orClauseCategoryFilter')]: '', + }; + return { - initialValues: ownProps.widgetConfig, + initialValues: ownProps.widgetConfig || INITIAL_VALUES, language: getLocale(state), languages: getActiveLanguages(state), contentTypes: getContentTypeList(state), @@ -39,17 +50,12 @@ export const mapStateToProps = (state, ownProps) => { pages: getSearchPagesRaw(state), categories: getCategoryTree(state), contentTemplates: getContentTemplateList(state), - selectedContentType: formValueSelector(formToUse)(state, putPrefixField('contentType')), - selectedCategories: formValueSelector(formToUse)(state, putPrefixField('categories')), - selectedInclusiveOr: formValueSelector(formToUse)(state, putPrefixField('orClauseCategoryFilter')), - widgetConfigFormData: getFormValues(formToUse)(state), defaultLanguageCode: getDefaultLanguage(state), }; }; export const mapDispatchToProps = (dispatch, ownProps) => { - const formToUse = get(ownProps, 'extFormName', ContentsQueryContainerId); - const parentField = get(ownProps, 'input.name', ''); + const parentField = get(ownProps, 'field.name', ''); const putPrefixField = field => (parentField !== '' ? `${parentField}.${field}` : field); return { onDidMount: () => { @@ -59,9 +65,10 @@ export const mapDispatchToProps = (dispatch, ownProps) => { dispatch(fetchSearchPages(nopage)); }, putPrefixField, + parentField, onSubmit: (values) => { const { - pageCode, frameId, widgetCode, history, intl, + pageCode, frameId, widgetCode, } = ownProps; const { contentTypeDetails, ...checkedValues } = values; if (values.modelId === '') delete checkedValues.modelId; @@ -71,46 +78,50 @@ export const mapDispatchToProps = (dispatch, ownProps) => { const configItem = Object.assign({ config: checkedValues }, { code: widgetCode }); dispatch(clearErrors()); - if ((isUndefined(values.modelId) || values.modelId === '') && isNull(contentTypeDetails.defaultContentModelList)) { + if ((isUndefined(values.modelId) || values.modelId === '') + && isNull(contentTypeDetails) + && isNull(contentTypeDetails.defaultContentModelList)) { dispatch(setVisibleModal(NoDefaultWarningModalId)); - } else { - dispatch(sendPutWidgetConfig(pageCode, frameId, configItem)).then((res) => { - if (res) { - dispatch(addToast( - intl.formatMessage({ id: 'widget.update.success' }), - TOAST_SUCCESS, - )); - history.push(routeConverter(ROUTE_APP_BUILDER_PAGE_CONFIG, { pageCode })); - } - }); } - }, - onResetFilterOption: (name, i) => ( - dispatch(change(formToUse, `${name}.[${i}].option`, '')) - ), - onChangeFilterAttribute: (name, i, value) => ( - dispatch(change(formToUse, `${name}.[${i}].attributeFilter`, value)) - ), - onChangeFilterValue: (name, i, value) => ( - dispatch(change(formToUse, `${name}.[${i}]`, value)) - ), - onChangeContentType: (contentType) => { + return dispatch(sendPutWidgetConfig(pageCode, frameId, configItem)); + }, + onResetFilterOption: (name, i, value, setFieldValue) => { + setFieldValue(`${name}.${i}.option`, ''); + setFieldValue(`${name}.${i}.key`, value); + }, + onChangeFilterAttribute: (name, i, value, setFieldValue) => { + setFieldValue(`${name}.${i}.attributeFilter`, value); + }, + onChangeFilterValue: (name, i, value, setFieldValue) => { + setFieldValue(`${name}.${i}`, value); + }, + setSelectedContentType: (contentType) => { + dispatch(fetchContentTemplatesByContentType(contentType.code)); + dispatch(setSelectedContentType(contentType)); + }, + onChangeContentType: (contentType, setFieldValue) => { if (contentType) { dispatch(fetchContentTemplatesByContentType(contentType)); dispatch(fetchContentType(contentType, false)) - .then(ctype => dispatch(change(formToUse, putPrefixField('contentTypeDetails'), ctype))); + .then(ctype => setFieldValue(putPrefixField('contentTypeDetails'), ctype)); } }, - onResetModelId: () => dispatch(change(formToUse, putPrefixField('modelId'), '')), - onToggleInclusiveOr: value => dispatch(change(formToUse, putPrefixField('orClauseCategoryFilter'), value === 'true' ? '' : 'true')), - onSave: () => { dispatch(setVisibleModal('')); dispatch(submit(ContentsQueryContainerId)); }, + onResetModelId: setFieldValue => setFieldValue(putPrefixField('modelId'), ''), + onToggleInclusiveOr: (value, setFieldValue) => setFieldValue(putPrefixField('orClauseCategoryFilter'), value === 'true' ? '' : 'true'), + onSave: (submitForm) => { + submitForm(); + dispatch(setVisibleModal('')); + }, onCancel: () => dispatch(setVisibleModal(ConfirmCancelModalID)), onDiscard: () => { dispatch(setVisibleModal('')); const { history, pageCode } = ownProps; history.push(routeConverter(ROUTE_APP_BUILDER_PAGE_CONFIG, { pageCode })); }, + continueWithDispatch: (dispatchValue) => { + dispatch(dispatchValue); + }, }; }; diff --git a/src/ui/widgets/config/WidgetConfigPage.js b/src/ui/widgets/config/WidgetConfigPage.js index e20c704e6..c9c4672a1 100644 --- a/src/ui/widgets/config/WidgetConfigPage.js +++ b/src/ui/widgets/config/WidgetConfigPage.js @@ -199,7 +199,7 @@ WidgetConfigPage.propTypes = { onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, intl: intlShape.isRequired, - history: PropTypes.shape({}).isRequired, + history: PropTypes.shape({}), }; WidgetConfigPage.defaultProps = { @@ -207,6 +207,7 @@ WidgetConfigPage.defaultProps = { widgetConfig: null, onDidMount: null, onWillUnmount: null, + history: {}, }; export default WidgetConfigPage; diff --git a/src/ui/widgets/config/WidgetConfigPanel.js b/src/ui/widgets/config/WidgetConfigPanel.js index cb94a3d13..27ef673fe 100644 --- a/src/ui/widgets/config/WidgetConfigPanel.js +++ b/src/ui/widgets/config/WidgetConfigPanel.js @@ -82,7 +82,7 @@ WidgetConfigPanel.propTypes = { framePos: PropTypes.number.isRequired, frameName: PropTypes.string.isRequired, pageCode: PropTypes.string.isRequired, - history: PropTypes.shape({}).isRequired, + history: PropTypes.shape({}), children: PropTypes.node.isRequired, buttons: PropTypes.node, }; @@ -91,6 +91,7 @@ WidgetConfigPanel.defaultProps = { widget: null, widgetConfig: null, buttons: null, + history: {}, }; export default WidgetConfigPanel;