diff --git a/mrs.developer.json b/mrs.developer.json index 20d80c5..ce25808 100644 --- a/mrs.developer.json +++ b/mrs.developer.json @@ -17,6 +17,7 @@ }, "volto-mosaic": { "url": "https://github.com/eea/volto-mosaic.git", + "branch": "volto-7", "path": "src" }, "volto-gridlayout": { diff --git a/package.json b/package.json index 9ef4f9e..b7bc4a5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "@eeacms/volto-widgets-view", "@eeacms/volto-slate-metadata-mentions", "@eeacms/volto-metadata-block", - "@eeacms/volto-tabs-block" + "@eeacms/volto-tabs-block", + "volto-mosaic", + "volto-addons", + "volto-datablocks", + "volto-tabsview" ], "scripts": { "start": "razzle start", @@ -113,7 +117,21 @@ "volto-slate": "github:eea/volto-slate#0.5.2" }, "devDependencies": { + "@babel/cli": "^7.8.4", + "@babel/core": "^7.8.4", + "@babel/plugin-proposal-decorators": "^7.8.3", + "@babel/plugin-proposal-do-expressions": "^7.8.3", + "@babel/plugin-proposal-export-default-from": "^7.8.3", + "@babel/plugin-proposal-export-namespace-from": "^7.8.3", + "@babel/plugin-proposal-function-bind": "^7.8.3", + "@babel/plugin-proposal-function-sent": "^7.8.3", + "@babel/plugin-proposal-logical-assignment-operators": "^7.8.3", + "@babel/plugin-proposal-numeric-separator": "^7.8.3", + "@babel/plugin-proposal-pipeline-operator": "^7.8.3", + "@babel/plugin-proposal-throw-expressions": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", "eslint-plugin-prettier": "3.1.3", + "mrs-developer": "^1.4.0", "prettier": "2.0.5", "stylelint": "13.3.3", "stylelint-config-idiomatic-order": "8.1.0", diff --git a/src/config.js b/src/config.js index 82e8bdd..0100903 100644 --- a/src/config.js +++ b/src/config.js @@ -14,21 +14,49 @@ import * as config from '@plone/volto/config'; +import { + installTableau, + installExpendableList, + installFolderListing, +} from 'volto-addons'; +import { applyEditForms as mosaicEditForms } from 'volto-mosaic/config'; +import { + applyConfig as eprtrConfig, + applyEditForms as eprtrEditForms, +} from './localconfig'; + +const addonConfig = [ + mosaicEditForms, + eprtrEditForms, + installTableau, + installExpendableList, + installFolderListing, + eprtrConfig, +].reduce((acc, apply) => apply(acc), config); + export const settings = { - ...config.settings, + ...addonConfig.settings, }; export const views = { - ...config.views, + ...addonConfig.views, }; export const widgets = { - ...config.widgets, + ...addonConfig.widgets, }; export const blocks = { - ...config.blocks, + ...addonConfig.blocks, +}; + +export const addonReducers = { ...addonConfig.addonReducers }; +export const addonRoutes = [...(addonConfig.addonRoutes || [])]; + +export const portlets = { + ...addonConfig.portlets, }; -export const addonReducers = { ...config.addonReducers }; -export const addonRoutes = [...(config.addonRoutes || [])]; \ No newline at end of file +export const editForms = { + ...addonConfig.editForms, +}; diff --git a/src/customizations/volto/components/manage/Form/ModalForm.jsx b/src/customizations/volto/components/manage/Form/ModalForm.jsx new file mode 100644 index 0000000..f028171 --- /dev/null +++ b/src/customizations/volto/components/manage/Form/ModalForm.jsx @@ -0,0 +1,380 @@ +/** + * Modal form component. + * @module components/manage/Form/ModalForm + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { keys, map, uniq, isFunction } from 'lodash'; +import { + Button, + Form as UiForm, + Header, + Menu, + Message, + Modal, +} from 'semantic-ui-react'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; + +import { Field, Icon } from '@plone/volto/components'; +import aheadSVG from '@plone/volto/icons/ahead.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; + +const messages = defineMessages({ + required: { + id: 'Required input is missing.', + defaultMessage: 'Required input is missing.', + }, + minLength: { + id: 'Minimum length is {len}.', + defaultMessage: 'Minimum length is {len}.', + }, + uniqueItems: { + id: 'Items must be unique.', + defaultMessage: 'Items must be unique.', + }, + save: { + id: 'Save', + defaultMessage: 'Save', + }, + cancel: { + id: 'Cancel', + defaultMessage: 'Cancel', + }, +}); + +/** + * Modal form container class. + * @class ModalForm + * @extends Component + */ +class ModalForm 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), + required: PropTypes.any, + }).isRequired, + title: PropTypes.string.isRequired, + formData: PropTypes.objectOf(PropTypes.any), + submitError: PropTypes.string, + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func, + open: PropTypes.bool, + submitLabel: PropTypes.string, + loading: PropTypes.bool, + className: PropTypes.string, + }; + + /** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + submitLabel: null, + onCancel: null, + formData: {}, + open: true, + loading: null, + submitError: null, + className: null, + }; + + /** + * Constructor + * @method constructor + * @param {Object} props Component properties + * @constructs ModalForm + */ + constructor(props) { + super(props); + this.state = { + currentTab: 0, + errors: {}, + formData: props.formData, + }; + this.selectTab = this.selectTab.bind(this); + this.onChangeField = this.onChangeField.bind(this); + this.onSubmit = this.onSubmit.bind(this); + } + + componentDidUpdate(prevProps, prevState) { + // Set value for select input to 'undefined' if there are different choices between current props and previous props + const { properties } = this.props.schema; + for (const property in properties) { + if (isFunction(properties[property].choices)) { + if ( + properties[property] + .choices(prevState.formData) + ?.concat() + .sort() + .join(',') !== + properties[property] + .choices(this.state.formData) + ?.concat() + .sort() + .join(',') + ) { + this.setState({ + formData: { + ...this.state.formData, + [property]: undefined, + }, + }); + } + } + } + } + + /** + * Change field handler + * @method onChangeField + * @param {string} id Id of the field + * @param {*} value Value of the field + * @returns {undefined} + */ + onChangeField(id, value) { + let formDataId = this.state.formData.id; + if (id === 'title') { + formDataId = value ? value.toLowerCase().split(' ').join('_') : undefined; + } + if (id === 'id') { + formDataId = value; + } + this.setState({ + formData: { + ...this.state.formData, + [id]: value, + id: formDataId, + }, + }); + } + + /** + * Submit handler + * @method onSubmit + * @param {Object} event Event object. + * @returns {undefined} + */ + onSubmit(event) { + event.preventDefault(); + const errors = {}; + map(this.props.schema.fieldsets, (fieldset) => + map(fieldset.fields, (fieldId) => { + const field = this.props.schema.properties[fieldId]; + const data = this.state.formData[fieldId]; + if ( + (isFunction(this.props.schema.required) && + this.props.schema.required(this.state.formData).indexOf(fieldId) !== + -1) || + (!isFunction(this.props.schema.required) && + this.props.schema.required.indexOf(fieldId) !== -1) + ) { + if (field.type !== 'boolean' && !data) { + errors[fieldId] = errors[field] || []; + errors[fieldId].push( + this.props.intl.formatMessage(messages.required), + ); + } + if (field.minLength && data.length < field.minLength) { + errors[fieldId] = errors[field] || []; + errors[fieldId].push( + this.props.intl.formatMessage(messages.minLength, { + len: field.minLength, + }), + ); + } + } + if (field.uniqueItems && data && uniq(data).length !== data.length) { + errors[fieldId] = errors[field] || []; + errors[fieldId].push( + this.props.intl.formatMessage(messages.uniqueItems), + ); + } + }), + ); + if (keys(errors).length > 0) { + this.setState({ + errors, + }); + } else { + let setFormDataCallback = (formData) => { + this.setState({ formData: formData }); + }; + this.props.onSubmit(this.state.formData, setFormDataCallback); + } + } + + /** + * Select tab handler + * @method selectTab + * @param {Object} event Event object. + * @param {number} index Selected tab index. + * @returns {undefined} + */ + selectTab(event, { index }) { + this.setState({ + currentTab: index, + }); + } + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + const { schema, onCancel } = this.props; + const currentFieldset = schema.fieldsets[this.state.currentTab]; + + const fields = map(currentFieldset.fields, (field) => { + const choices = isFunction(schema.properties[field]?.choices) + ? schema.properties[field].choices(this.state.formData) + : schema.properties[field]?.choices; + const disabled = isFunction(schema.properties[field]?.disabled) + ? schema.properties[field].disabled(this.state.formData) + : false; + const required = isFunction(schema.required) + ? (field && + schema.required(this.state.formData)?.indexOf(field) !== -1) || + false + : (field && schema.required?.indexOf(field) !== -1) || false; + const title = isFunction(schema.properties[field]?.title) + ? schema.properties[field].title(this.state.formData) + : schema.properties[field]?.title; + const type = isFunction(schema.properties[field]?.type) + ? schema.properties[field].type(this.state.formData) + : schema.properties[field]?.type; + const items = isFunction(schema.properties[field]?.items) + ? schema.properties[field].items(this.state.formData) + : schema.properties[field]?.items; + const description = isFunction(schema.properties[field]?.description) + ? schema.properties[field].description(this.state.formData) + : schema.properties[field]?.description; + const value = choices + ? choices + .map((choice) => choice[0] === this.state.formData[field]) + .includes(true) + ? this.state.formData[field] + : null + : this.state.formData[field]; + + return { + ...schema.properties[field], + id: field, + value: value, + onChange: this.onChangeField, + choices, + disabled, + required, + title, + type, + items, + description, + }; + }); + const state_errors = keys(this.state.errors).length > 0; + return ( + +
{this.props.title}
+ + + + {state_errors ? ( + + ) : ( + '' + )} +
{this.props.submitError}
+
+ {schema.fieldsets.length > 1 && ( + + {map(schema.fieldsets, (item, index) => ( + + {item.title} + + ))} + + )} + {fields.map( + (field) => + !field.disabled && ( + + ), + )} +
+
+ + + + )} + + {map( + value.fieldsets?.[this.state.currentFieldset]?.fields, + (field, index) => ( + + ), + )} + + + + +
+ +
+
+ +
+
+
+
+
+ + {(this.state.addField !== null || this.state.editField !== null) && ( + + )} + {this.state.addFieldset !== null && ( + + )} + {this.state.editFieldset !== null && ( + + )} + {this.state.deleteFieldset !== null && ( + + )} + {this.state.deleteField !== null && ( + + )} + + ); + } +} + +export default compose( + DragDropContext(HTML5Backend), + injectIntl, + connect((state, props) => ({ + value: JSON.parse(props.value), + })), +)(SchemaWidget); diff --git a/src/customizations/volto/components/manage/Widgets/SchemaWidgetFieldset.jsx b/src/customizations/volto/components/manage/Widgets/SchemaWidgetFieldset.jsx new file mode 100644 index 0000000..999f085 --- /dev/null +++ b/src/customizations/volto/components/manage/Widgets/SchemaWidgetFieldset.jsx @@ -0,0 +1,120 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/** + * Schema widget fieldset. + * @module components/manage/Widgets/SchemaWidgetFieldset + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { DragSource, DropTarget } from 'react-dnd'; +import { Icon } from 'semantic-ui-react'; + +/** + * Schema widget fieldset component. + * @function SchemaWidgetFieldset + * @returns {string} Markup of the component. + */ +export const SchemaWidgetFieldsetComponent = ({ + connectDragSource, + connectDragPreview, + connectDropTarget, + isDragging, + title, + order, + active, + onShowEditFieldset, + onShowDeleteFieldset, + onClick, +}) => + connectDropTarget( + connectDragPreview( +
onClick(order)} + > + {connectDragSource( +