@plone/volto
Version:
Volto
1,876 lines (1,761 loc) • 51.5 kB
JSX
/**
* SchemaWidget component.
* @module components/manage/Widgets/SchemaWidget
*/
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import PropTypes from 'prop-types';
import concat from 'lodash/concat';
import find from 'lodash/find';
import findIndex from 'lodash/findIndex';
import isArray from 'lodash/isArray';
import isString from 'lodash/isString';
import keys from 'lodash/keys';
import omit from 'lodash/omit';
import slice from 'lodash/slice';
import without from 'lodash/without';
import move from 'lodash-move';
import { Confirm, Form, Grid, Icon, Message, Segment } from 'semantic-ui-react';
import { defineMessages, injectIntl } from 'react-intl';
import config from '@plone/volto/registry';
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import { slugify } from '@plone/volto/helpers/Utils/Utils';
import { getVocabulary } from '@plone/volto/actions/vocabularies/vocabularies';
import SchemaWidgetFieldset from '@plone/volto/components/manage/Widgets/SchemaWidgetFieldset';
import { Field, ModalForm } from '@plone/volto/components/manage/Form';
const messages = defineMessages({
add: {
id: 'Add',
defaultMessage: 'Add',
},
addField: {
id: 'Add field',
defaultMessage: 'Add field',
},
addFieldset: {
id: 'Add fieldset',
defaultMessage: 'Add fieldset',
},
parentFieldSet: {
id: 'Parent fieldset',
defaultMessage: 'Parent fieldset',
},
editField: {
id: 'Edit field',
defaultMessage: 'Edit field',
},
editFieldset: {
id: 'Edit fieldset',
defaultMessage: 'Edit fieldset',
},
default: {
id: 'Default',
defaultMessage: 'Default',
},
defaultValue: {
id: 'Default value',
defaultMessage: 'Default value',
},
placeholder: {
id: 'Placeholder',
defaultMessage: 'Placeholder',
},
idTitle: {
id: 'Short Name',
defaultMessage: 'Short Name',
},
idDescription: {
id: 'Used for programmatic access to the fieldset.',
defaultMessage: 'Used for programmatic access to the fieldset.',
},
choices: {
id: 'Possible values',
defaultMessage: 'Possible values (Enter allowed choices one per line).',
},
string: {
id: 'String',
defaultMessage: 'String',
},
text: {
id: 'Text',
defaultMessage: 'Text',
},
richtext: {
id: 'Richtext',
defaultMessage: 'Richtext',
},
checkbox: {
id: 'Checkbox',
defaultMessage: 'Checkbox',
},
selection: {
id: 'Selection',
defaultMessage: 'Selection',
},
type: {
id: 'Type',
defaultMessage: 'Type',
},
title: {
id: 'Title',
defaultMessage: 'Title',
},
description: {
id: 'Description',
defaultMessage: 'Description',
},
queryParameterName: {
id: 'Query Parameter Name',
defaultMessage: 'Query Parameter Name',
},
queryParameterNameDescription: {
id: 'Fills the value of the form field with the value supplied by a query parameter inside the URL with the given name.',
defaultMessage:
'Fills the value of the form field with the value supplied by a query parameter inside the URL with the given name.',
},
required: {
id: 'Required',
defaultMessage: 'Required',
},
minLength: {
id: 'minLength',
defaultMessage: 'Minimum Length',
},
maxLength: {
id: 'maxLength',
defaultMessage: 'Maximum Length',
},
minimum: {
id: 'minimum',
defaultMessage: 'Start of the range',
},
maximum: {
id: 'maximum',
defaultMessage: 'End of the range (including the value itself)',
},
size: {
id: 'size',
defaultMessage: 'Maximum size of the file in bytes',
},
accept: {
id: 'accept',
defaultMessage: 'File types allowed',
},
deleteFieldset: {
id: 'Are you sure you want to delete this fieldset including all fields?',
defaultMessage:
'Are you sure you want to delete this fieldset including all fields?',
},
deleteField: {
id: 'Are you sure you want to delete this field?',
defaultMessage: 'Are you sure you want to delete this field?',
},
error: {
id: 'Error',
defaultMessage: 'Error',
},
});
/**
* Makes a list of fieldset types formatted for select widget
* @param {Object[]} listOfTypes array of strings
* @param {*} intl
* @returns {Object[]} example [['default', 'default']]
*/
const makeFieldsetList = (listOfFieldsets, intl) => {
const result = listOfFieldsets.map((type) => [type.id, type.title]);
return result;
};
// Register field factory properties utilities
config.registerUtility({
name: 'Rich Text',
type: 'fieldFactoryProperties',
method: (intl) => ({
maxLength: {
type: 'integer',
title: intl.formatMessage(messages.maxLength),
},
default: {
title: intl.formatMessage(messages.defaultValue),
widget: 'richtext',
type: 'string',
},
}),
});
['URL', 'Password', 'label_password_field', 'Email', 'label_email'].forEach(
(factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
minLength: {
type: 'integer',
title: intl.formatMessage(messages.minLength),
},
maxLength: {
type: 'integer',
title: intl.formatMessage(messages.maxLength),
},
default: {
type: 'string',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
},
);
['Integer', 'label_integer_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
minimum: {
type: 'integer',
title: intl.formatMessage(messages.minimum),
},
maximum: {
type: 'integer',
title: intl.formatMessage(messages.maximum),
},
default: {
type: 'integer',
title: intl.formatMessage(messages.default),
},
}),
});
});
[
'Floating-point number',
'label_float_field',
'JSONField',
'Relation Choice',
'Relation List',
].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({}),
});
});
['Yes/No', 'label_boolean_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
default: {
type: 'string',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
});
['Date/Time', 'label_datetime_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
default: {
type: 'string',
widget: 'datetime',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
});
['Date', 'label_date_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
default: {
type: 'string',
widget: 'date',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
});
['time'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
default: {
type: 'string',
widget: 'time',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
});
['File', 'File Upload', 'Image'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
size: {
type: 'integer',
title: intl.formatMessage(messages.size),
},
accept: {
type: 'string',
title: intl.formatMessage(messages.accept),
},
}),
});
});
['Multiple Choice', 'label_multi_choice_field', 'checkbox_group'].forEach(
(factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
values: {
type: 'string',
title: intl.formatMessage(messages.choices),
widget: 'textarea',
},
default: {
type: 'string',
widget: 'textarea',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
},
);
['Choice', 'label_choice_field', 'radio_group'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryProperties',
method: (intl) => ({
values: {
type: 'string',
title: intl.formatMessage(messages.choices),
widget: 'textarea',
},
default: {
type: 'string',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
});
config.registerUtility({
name: 'static_text',
type: 'fieldFactoryProperties',
method: (intl) => ({
default: {
title: intl.formatMessage(messages.text),
widget: 'richtext',
type: 'string',
},
}),
});
config.registerUtility({
name: 'number',
type: 'fieldFactoryProperties',
method: (intl) => ({
default: {
type: 'number',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
config.registerUtility({
name: 'hidden',
type: 'fieldFactoryProperties',
method: (intl) => ({
default: {
type: 'string',
title: intl.formatMessage(messages.defaultValue),
},
}),
});
config.registerUtility({
name: 'textarea',
type: 'fieldFactoryProperties',
method: (intl) => ({
minLength: {
type: 'integer',
title: intl.formatMessage(messages.minLength),
},
maxLength: {
type: 'integer',
title: intl.formatMessage(messages.maxLength),
},
default: {
type: 'string',
widget: 'textarea',
title: intl.formatMessage(messages.defaultValue),
},
placeholder: {
type: 'string',
title: intl.formatMessage(messages.placeholder),
},
}),
});
// Register field factory initial data utilities
['Date/Time', 'label_datetime_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'datetime',
factory,
}),
});
});
['Date', 'label_date_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'date',
factory,
}),
});
});
config.registerUtility({
name: 'time',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'time',
factory: 'time',
}),
});
['Email', 'label_email'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'email',
id: 'email',
factory,
}),
});
});
['File', 'File Upload'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'object',
factory,
}),
});
});
['Floating-point number', 'label_float_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'number',
factory,
}),
});
});
['Integer', 'label_integer_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'integer',
factory,
}),
});
});
config.registerUtility({
name: 'Image',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'object',
factory: 'Image',
}),
});
config.registerUtility({
name: 'JSONField',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'dict',
widget: 'json',
factory: 'JSONField',
}),
});
['Multiple Choice', 'label_multi_choice_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'array',
factory,
}),
});
});
config.registerUtility({
name: 'Relation List',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'array',
factory: 'Relation List',
}),
});
['Choice', 'label_choice_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
choices: [],
factory,
}),
});
});
config.registerUtility({
name: 'Relation Choice',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
factory: 'Relation Choice',
}),
});
['Password', 'label_password_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'password',
factory,
}),
});
});
config.registerUtility({
name: 'Rich Text',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'richtext',
factory: 'Rich Text',
}),
});
config.registerUtility({
name: 'URL',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'url',
factory: 'URL',
}),
});
['Yes/No', 'label_boolean_field'].forEach((factory) => {
config.registerUtility({
name: factory,
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'boolean',
factory,
}),
});
});
config.registerUtility({
name: 'static_text',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'object',
widget: 'static_text',
factory: 'static_text',
}),
});
config.registerUtility({
name: 'hidden',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'hidden',
factory: 'hidden',
}),
});
config.registerUtility({
name: 'number',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'number',
factory: 'number',
}),
});
config.registerUtility({
name: 'radio_group',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
choices: [],
widget: 'radio_group',
factory: 'radio_group',
}),
});
config.registerUtility({
name: 'checkbox_group',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'array',
widget: 'checkbox_group',
factory: 'checkbox_group',
}),
});
config.registerUtility({
name: 'textarea',
type: 'fieldFactoryInitialData',
method: (intl) => ({
type: 'string',
widget: 'textarea',
factory: 'textarea',
}),
});
/**
* schemaField used for modal form, when editing a field
* - based on the factory a set of fields is presented
* - fields can be moved to another fieldset
* @param {string} factory - the kind of field
* @param {Object} intl
* @param {*} fieldsets
* @param {Boolean} allowEditId
* @param {Boolean} allowEditQueryParameter
* @param {Boolean} allowEditPlaceholder
* @param {Object} extraFields
* @return {Object} - schema
*/
const schemaField = (
factory,
intl,
fieldsets,
allowEditId,
allowEditQueryParameter,
allowEditPlaceholder,
extraFields = {},
) => {
const utility = config.getUtility({
name: factory,
type: 'fieldFactoryProperties',
});
const properties = utility.method
? utility.method(intl)
: {
minLength: {
type: 'integer',
title: intl.formatMessage(messages.minLength),
},
maxLength: {
type: 'integer',
title: intl.formatMessage(messages.maxLength),
},
default: {
type: 'string',
title: intl.formatMessage(messages.defaultValue),
},
...(allowEditPlaceholder
? {
placeholder: {
type: 'string',
title: intl.formatMessage(messages.placeholder),
},
}
: {}),
};
return {
fieldsets: [
{
id: 'default',
title: 'default',
fields: [
...keys(extraFields),
...(allowEditId ? ['id'] : []),
...['title', 'description', 'parentFieldSet'],
...(allowEditQueryParameter ? ['queryParameterName'] : []),
...keys(properties),
...['required'],
],
},
],
properties: {
...extraFields,
...(allowEditId
? {
id: {
type: 'string',
title: intl.formatMessage(messages.idTitle),
},
}
: {}),
title: {
type: 'string',
title: intl.formatMessage(messages.title),
},
description: {
type: 'string',
widget: 'textarea',
title: intl.formatMessage(messages.description),
},
parentFieldSet: {
type: 'string',
title: intl.formatMessage(messages.parentFieldSet),
choices: makeFieldsetList(fieldsets),
},
...(allowEditQueryParameter
? {
queryParameterName: {
type: 'string',
title: intl.formatMessage(messages.queryParameterName),
description: intl.formatMessage(
messages.queryParameterNameDescription,
),
},
}
: {}),
required: {
type: 'boolean',
title: intl.formatMessage(messages.required),
},
...properties,
},
required: ['type', 'title'],
};
};
/**
* schema for adding a new field
* @param {Object} intl
*/
const fieldsetSchema = (intl) => ({
fieldsets: [
{
id: 'default',
title: intl.formatMessage(messages.default),
fields: ['title', 'id'],
},
],
properties: {
id: {
type: 'string',
title: intl.formatMessage(messages.idTitle),
description: intl.formatMessage(messages.idDescription),
},
title: {
type: 'string',
title: intl.formatMessage(messages.title),
},
},
required: ['id', 'title'],
});
/**
* 'plone.dexterity.schema.generated' is considered user created
* @param {Object} field
*/
const isEditable = (field) =>
!field.behavior || field.behavior.includes('generated');
const getItemStyle = (isDragging, draggableStyle) => ({
// some basic styles to make the items look a bit nicer
userSelect: 'none',
// change background colour if dragging
background: isDragging ? 'white' : 'transparent',
// styles we need to apply on draggable
...draggableStyle,
});
const getTabStyle = (isDraggingOver) => ({
background: isDraggingOver ? '#f4f4f4' : 'transparent',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
});
const getFieldStyle = (isDraggingOver) => ({
background: isDraggingOver ? '#f4f4f4' : 'transparent',
});
/**
* will transform a string with new lines in an array for each item on a line
* @param {string} textarea - has '\r\n' characters
*/
const formatTextareaToArray = (textarea) => {
const values =
textarea && textarea
? textarea
.split(/(\r\n|\n|\r)/gm)
.map((elem) => elem.trim())
.filter((elem) => elem !== '')
: null;
return values ? { values } : {};
};
const formatArrayToTextarea = (props) => {
if (props?.values) {
return props.values.join('\n');
}
if (props?.choices) {
return props.choices.map((elem) => elem[0]).join('\n');
}
if (props?.items?.choices) {
return props.items.choices.map((elem) => elem[0]).join('\n');
}
return '';
};
const formatTextareaToChoices = (textarea, multiple) => {
const choices =
textarea && textarea
? textarea
.split(/(\r\n|\n|\r)/gm)
.map((elem) => elem.trim())
.filter((elem) => elem !== '')
.map((elem) => [elem, elem])
: null;
if (!multiple) {
return choices ? { choices } : {};
}
const items = choices ? { choices: choices } : {};
return items ? { items } : {};
};
/**
* SchemaWidget component class.
* @class SchemaWidget
* @extends Component
*/
class SchemaWidget extends Component {
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
static propTypes = {
/**
* Id of the field
*/
id: PropTypes.string.isRequired,
/**
* Title of the field
*/
required: PropTypes.bool,
/**
* Value of the field
*/
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* List of error messages
*/
error: PropTypes.arrayOf(PropTypes.string),
/**
* Filter for factory choices
*/
filterFactory: PropTypes.arrayOf(PropTypes.string),
/**
* Additional factories
*/
additionalFactory: PropTypes.arrayOf(PropTypes.object),
/**
* Allow editing of the id
*/
allowEditId: PropTypes.bool,
/**
* Allow editing of the query parameter
*/
allowEditQueryParameter: PropTypes.bool,
/**
* Allow editing of the placeholder
*/
allowEditPlaceholder: PropTypes.bool,
/**
* On change handler
*/
onChange: PropTypes.func.isRequired,
/**
* Get vocabulary action
*/
getVocabulary: PropTypes.func.isRequired,
};
/**
* Default properties
* @property {Object} defaultProps Default properties.
* @static
*/
static defaultProps = {
required: false,
value: {},
error: [],
filterFactory: null,
additionalFactory: null,
allowEditId: false,
allowEditQueryParameter: false,
allowEditPlaceholder: false,
};
/**
* Constructor
* @method constructor
* @param {Object} props Component properties
* @constructs WysiwygEditor
*/
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onChangeDefaultValue = this.onChangeDefaultValue.bind(this);
this.onAddField = this.onAddField.bind(this);
this.onAddFieldset = this.onAddFieldset.bind(this);
this.onEditField = this.onEditField.bind(this);
this.onEditFieldset = this.onEditFieldset.bind(this);
this.onDeleteFieldset = this.onDeleteFieldset.bind(this);
this.onDeleteField = this.onDeleteField.bind(this);
this.onShowAddField = this.onShowAddField.bind(this);
this.onShowAddFieldset = this.onShowAddFieldset.bind(this);
this.onShowEditFieldset = this.onShowEditFieldset.bind(this);
this.onShowEditField = this.onShowEditField.bind(this);
this.onShowDeleteFieldset = this.onShowDeleteFieldset.bind(this);
this.onShowDeleteField = this.onShowDeleteField.bind(this);
this.onSetCurrentFieldset = this.onSetCurrentFieldset.bind(this);
this.onOrderField = this.onOrderField.bind(this);
this.onOrderFieldset = this.onOrderFieldset.bind(this);
this.onCancel = this.onCancel.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
this.state = {
addField: null,
addFieldset: null,
editFieldset: null,
editField: null,
deleteFieldset: null,
deleteField: null,
currentFieldset: 0,
};
}
/**
* Component did mount
* @method componentDidMount
* @returns {undefined}
*/
componentDidMount() {
this.props.getVocabulary({
vocabNameOrURL: 'Fields',
size: -1,
subrequest: 'schemawidget',
});
}
/**
* Add field handler
* @method onAddField
* @param {Object} values Form values
* @returns {undefined}
*/
onAddField(values) {
const fieldId = slugify(
values.id || values.title,
keys(this.props.value.properties),
);
const currentFieldsetFields =
this.props.value.fieldsets[this.state.currentFieldset].fields;
const hasChangeNote = currentFieldsetFields.indexOf('changeNote') > -1;
const newFieldsetFields = hasChangeNote
? [
...currentFieldsetFields.slice(0, currentFieldsetFields.length - 1),
fieldId,
currentFieldsetFields[currentFieldsetFields.length - 1],
]
: [...currentFieldsetFields, fieldId];
const utility = config.getUtility({
name: values.factory,
type: 'fieldFactoryInitialData',
});
const multiple =
values.factory === 'Multiple Choice' ||
values.factory === 'label_multi_choice_field';
const initialData = utility.method
? omit(utility.method(this.props.intl), ['id'])
: {
type: 'string',
factory: values.factory,
};
this.onChange({
...this.props.value,
fieldsets: [
...slice(this.props.value.fieldsets, 0, this.state.currentFieldset),
{
...this.props.value.fieldsets[this.state.currentFieldset],
fields: newFieldsetFields,
},
...slice(this.props.value.fieldsets, this.state.currentFieldset + 1),
],
properties: {
...this.props.value.properties,
[fieldId]: {
id: fieldId,
...omit(initialData, ['required']),
...omit(values, ['factory', 'required', 'id', 'parentFieldset']),
...formatTextareaToArray(values.values),
...formatTextareaToChoices(values.values, multiple),
},
},
required: [
...this.props.value.required,
...(values.required || initialData.required ? [fieldId] : []),
],
});
this.onCancel();
}
/**
* Add fieldset handler
* @method onAddFieldset
* @param {Object} values Form values
* @returns {undefined}
*/
onAddFieldset(values) {
this.onChange({
...this.props.value,
fieldsets: [
...this.props.value.fieldsets,
{
...values,
fields: [],
},
],
});
this.onCancel();
}
/**
* Edit fieldset handler
* @method onEditFieldset
* @param {Object} values Form values
* @returns {undefined}
*/
onEditFieldset(values) {
values.fields =
values.fields ||
this.props.value.fieldsets[this.state.editFieldset]?.fields ||
[];
this.onChange({
...this.props.value,
fieldsets: [
...slice(this.props.value.fieldsets, 0, this.state.editFieldset),
values,
...slice(this.props.value.fieldsets, this.state.editFieldset + 1),
],
});
this.onCancel();
}
/**
* Recreates the fieldset structure
* will move change name of the field if needed and
* change fieldset if changed
* @param {Object[]} fieldsets
* @param {string} parentFieldSet - id
* @param {number} currentFieldset - index
* @param {Object} oldfieldId
* @param {Object} newfieldId
* @returns {Object[]} fieldsets
*/
editFieldset(
fieldsets,
parentFieldSet,
currentFieldset,
oldfieldId,
newfieldId,
) {
const moveToFieldsetWithNewName = () => {
const newParentFieldsetIndex = fieldsets.findIndex(
(field) => field.id === parentFieldSet,
);
const indexOfChangeNote =
fieldsets[newParentFieldsetIndex].fields.indexOf('changeNote');
// remove from current fieldset
const fieldsetsWithoutField = [
...slice(fieldsets, 0, currentFieldset),
{
...fieldsets[currentFieldset],
fields: fieldsets[currentFieldset].fields.filter(
(fieldId) => fieldId !== oldfieldId,
),
},
...slice(fieldsets, currentFieldset + 1),
];
const fieldsOfNewFieldset =
indexOfChangeNote > -1
? [
...fieldsetsWithoutField[newParentFieldsetIndex].fields.slice(
0,
indexOfChangeNote + 1,
),
oldfieldId,
fieldsetsWithoutField[newParentFieldsetIndex].fields[
indexOfChangeNote
],
]
: [
...fieldsetsWithoutField[newParentFieldsetIndex].fields,
oldfieldId,
];
// add to new fieldset
const fieldsetsWithField = [
...slice(fieldsetsWithoutField, 0, newParentFieldsetIndex),
{
...fieldsetsWithoutField[newParentFieldsetIndex],
fields: fieldsOfNewFieldset,
},
...slice(fieldsetsWithoutField, newParentFieldsetIndex + 1),
];
return fieldsetsWithField;
};
const changeNameInFieldset = () => {
return [
...slice(fieldsets, 0, currentFieldset),
{
...fieldsets[currentFieldset],
fields: fieldsets[currentFieldset].fields.map((field) =>
field === oldfieldId ? newfieldId : field,
),
},
...slice(fieldsets, currentFieldset + 1),
];
};
const result =
parentFieldSet !== fieldsets[currentFieldset].id
? moveToFieldsetWithNewName()
: changeNameInFieldset();
return result;
}
/**
* Edit field handler
* recreates the schema based on field changes (properties, name, fieldset)
* @method onEditField
* @param {Object} values Field values
* @returns {undefined}
*/
onEditField(values) {
let formattedValues = { ...values };
const listOfProp = ['minLength', 'maxLength', 'minimum', 'maximum'];
listOfProp.forEach((prop) => {
formattedValues = {
...formattedValues,
...{ [prop]: values[prop] ? parseFloat(values[prop]) : undefined },
};
});
const multiple =
this.props.value.properties[this.state.editField.id]?.factory ===
'Multiple Choice' ||
this.props.value.properties[this.state.editField.id]?.factory ===
'label_multi_choice_field';
let fieldsets = this.props.value.fieldsets;
if (this.state.editField.id !== formattedValues.id) {
this.props.value.fieldsets[this.state.currentFieldset].fields =
this.props.value.fieldsets[this.state.currentFieldset].fields.map(
(field) =>
field === this.state.editField.id ? formattedValues.id : field,
);
const index = isArray(this.props.value.required)
? this.props.value.required.indexOf(formattedValues.id)
: -1;
if (index > -1) {
this.props.value.required[index] = formattedValues.id;
}
}
if (formattedValues.parentFieldSet) {
fieldsets = this.editFieldset(
this.props.value.fieldsets,
formattedValues.parentFieldSet,
this.state.currentFieldset,
this.state.editField.id,
formattedValues.id,
);
}
const result = {
...this.props.value,
fieldsets,
properties: {
...omit(this.props.value.properties, [this.state.editField.id]),
[formattedValues.id]: {
...this.props.value.properties[this.state.editField.id],
...omit(formattedValues, ['parentFieldSet']),
...formatTextareaToArray(formattedValues.values),
...formatTextareaToChoices(formattedValues.values, multiple),
},
},
required: formattedValues.required
? concat(without(this.props.value.required, this.state.editField.id), [
formattedValues.id,
])
: without(this.props.value.required, this.state.editField.id),
};
this.onChange(result);
this.onCancel();
}
/**
* Delete fieldset handler
* @method onDeleteFieldset
* @returns {undefined}
*/
onDeleteFieldset() {
if (this.state.currentFieldset > this.props.value.fieldsets.length - 2) {
this.setState({
currentFieldset: this.state.currentFieldset - 1,
});
}
this.onChange({
...this.props.value,
fieldsets: [
...slice(this.props.value.fieldsets, 0, this.state.deleteFieldset),
...slice(this.props.value.fieldsets, this.state.deleteFieldset + 1),
],
properties: omit(
this.props.value.properties,
this.props.value.fieldsets[this.state.deleteFieldset].fields,
),
});
this.onCancel();
}
/**
* Delete field handler
* @method onDeleteField
* @returns {undefined}
*/
onDeleteField() {
this.onChange({
...this.props.value,
fieldsets: [
...slice(this.props.value.fieldsets, 0, this.state.currentFieldset),
{
...this.props.value.fieldsets[this.state.currentFieldset],
fields: without(
this.props.value.fieldsets[this.state.currentFieldset].fields,
this.state.deleteField,
),
},
...slice(this.props.value.fieldsets, this.state.currentFieldset + 1),
],
properties: omit(this.props.value.properties, [this.state.deleteField]),
required: without(
this.props.value.required || [],
this.state.deleteField,
),
});
this.onCancel();
}
/**
* Change handler
* @method onChange
* @param {Object} value New schema
* @returns {undefined}
*/
onChange(value) {
this.props.onChange(this.props.id, value);
}
/**
* Change default value handler
* @method onChangeDefaultValue
* @param {string} fieldId
* @param {string} fieldValue
*/
onChangeDefaultValue(fieldId, fieldValue) {
// Default values can have irreversible consequence, thus skip it for now.
// const value = { default: fieldValue }
const value = {};
const fieldMerge = {
...this.props.value.properties[fieldId],
...value,
};
const propsMerge = {
...this.props.value.properties,
...{ [fieldId]: fieldMerge },
};
this.onChange({
...this.props.value,
properties: propsMerge,
});
}
/**
* Cancel handler
* @method onCancel
* @returns {undefined}
*/
onCancel() {
this.setState({
addField: null,
addFieldset: null,
editFieldset: null,
editField: null,
deleteFieldset: null,
deleteField: null,
});
}
/**
* Show add field handler
* @method onShowAddField
* @returns {undefined}
*/
onShowAddField(event) {
this.setState({
addField: '',
});
event.preventDefault();
}
/**
* Show add fieldset handler
* @method onShowAddFieldset
* @returns {undefined}
*/
onShowAddFieldset(event) {
this.setState({
addFieldset: true,
});
event.preventDefault();
}
/**
* Show edit fieldset handler
* @method onShowEditFieldset
* @param {Number} index Index of fieldset
* @returns {undefined}
*/
onShowEditFieldset(index) {
this.setState({
editFieldset: index,
});
}
/**
* Show edit field handler
* @method onShowEditField
* @param {string} id Id of field
* @param {Object} schema Schema of the field
* @returns {undefined}
*/
onShowEditField(id, schema) {
return this.setState({
editField: {
id,
},
});
}
/**
* Show delete fieldset handler
* @method onShowDeleteFieldset
* @param {Number} index Index of fieldset
* @param {Object} event Event object
* @returns {undefined}
*/
onShowDeleteFieldset(index) {
this.setState({
deleteFieldset: index,
});
}
/**
* Show delete field handler
* @method onShowDeleteField
* @param {String} field Field to delete
* @param {Object} event Event object
* @returns {undefined}
*/
onShowDeleteField(field) {
this.setState({
deleteField: field,
});
}
/**
* Set current fieldset handler
* @method onSetCurrentFieldset
* @param {Number} index Index of fieldset
* @returns {undefined}
*/
onSetCurrentFieldset(index) {
this.setState({
currentFieldset: index,
});
}
/**
* On order fieldset
* @method onOrderField
* @param {number} index Index
* @param {number} delta Delta
* @returns {undefined}
*/
onOrderField(index, delta) {
this.onChange({
...this.props.value,
fieldsets: [
...slice(this.props.value.fieldsets, 0, this.state.currentFieldset),
{
...this.props.value.fieldsets[this.state.currentFieldset],
fields: move(
this.props.value.fieldsets[this.state.currentFieldset].fields,
index,
delta,
),
},
...slice(this.props.value.fieldsets, this.state.currentFieldset + 1),
],
});
}
/**
* On order fieldset
* @method onOrderFieldset
* @param {number} index Index
* @param {number} delta Delta
* @returns {undefined}
*/
onOrderFieldset(index, delta) {
const schema = {
...this.props.value,
fieldsets: move(this.props.value.fieldsets, index, delta),
};
this.setState({
currentFieldset: findIndex(schema.fieldsets, {
id: schema.fieldsets[this.state.currentFieldset].id,
}),
});
this.onChange(schema);
}
/**
* Set current fieldset handler
* @method onDragEnd
* @param {Number} index Index of fieldset
* @returns {undefined}
*/
onDragEnd(result) {
if (
result.destination &&
result.destination.droppableId === 'fields-schema-edit'
) {
this.onOrderField(result.source.index, result.destination.index);
}
if (
result.destination &&
result.destination.droppableId === 'tabs-schema-edit'
) {
this.onOrderFieldset(result.source.index, result.destination.index);
}
}
/**
* Render method.
* @method render
* @returns {string} Markup for the component.
*/
render() {
const { additionalFactory, error, reactBeautifulDnd, filterFactory } =
this.props;
const { Draggable, DragDropContext, Droppable } = reactBeautifulDnd;
if (!this.props.value) {
return '';
}
const choices = [
...this.props.fields,
...(this.props.additionalFactory || []),
];
let editFieldType = '';
if (this.state.editField) {
let factory =
this.props.value.properties[this.state.editField.id].factory;
if (factory.value) {
factory = factory.value;
}
const fieldType = find(choices, {
value: factory !== '' ? factory : 'label_text_field',
});
editFieldType = fieldType
? this.props.intl.formatMessage({
id: fieldType.value,
defaultMessage: fieldType.label,
})
: '';
}
const nonUserCreatedFields = this.props.value.fieldsets[
this.state.currentFieldset
].fields.filter(
(fieldId) =>
!isEditable(this.props.value.properties[fieldId]) &&
fieldId !== 'changeNote',
);
const hasChangeNote =
this.props.value.fieldsets[this.state.currentFieldset].fields.indexOf(
'changeNote',
) > -1;
const userCreatedFieldsStartingIndex = nonUserCreatedFields.length;
const lastUserCreatedFieldsIndex = hasChangeNote
? this.props.value.fieldsets[this.state.currentFieldset].fields.length - 1
: this.props.value.fieldsets[this.state.currentFieldset].fields.length;
// fields that were not created by the user, but are part of a behavior
const makeNonUserFields = () =>
this.props.value.fieldsets[this.state.currentFieldset].fields
.slice(0, userCreatedFieldsStartingIndex)
.map((field, index) => (
<div
style={{ background: '#c7d5d859' }}
key={`${field}-${this.state.currentFieldset}-${index}`}
>
<Field
{...this.props.value.properties[field]}
id={field}
required={this.props.value.required.indexOf(field) !== -1}
widgets={this.props.widgets}
onEdit={this.onShowEditField}
draggable={false}
isDisabled={true}
order={index}
onDelete={this.onShowDeleteField}
onChange={this.onChangeDefaultValue}
value={this.props.value.properties[field].default}
/>
</div>
));
// fields created by the user
const makeUserFields = () =>
this.props.value.fieldsets[this.state.currentFieldset].fields
.slice(userCreatedFieldsStartingIndex, lastUserCreatedFieldsIndex)
.map((field, index) => (
<Draggable
draggableId={field}
index={userCreatedFieldsStartingIndex + index}
key={`${field}-${this.state.currentFieldset}-${index}`}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style,
)}
>
<Field
{...this.props.value.properties[field]}
id={field}
required={
this.props.value.required &&
this.props.value.required.indexOf(field) !== -1
}
widgets={this.props.widgets}
onEdit={this.onShowEditField}
draggable={true}
isDisabled={false}
order={index}
onDelete={this.onShowDeleteField}
onChange={this.onChangeDefaultValue}
key={`${field}-${this.state.currentFieldset}-${index}`}
value={this.props.value.properties[field].default}
/>
</div>
)}
</Draggable>
));
const canAddFields =
this.state.currentFieldset === 0 ||
!this.props.value.fieldsets[this.state.currentFieldset].behavior ||
this.props.value.fieldsets[this.state.currentFieldset].behavior.includes(
'generated',
);
const utility = config.getUtility({
name: this.state.addField,
type: 'fieldFactoryInitialData',
});
const id = utility?.method ? utility.method(this.props.intl).id : undefined;
return (
<div>
<Segment.Group
style={{
margin: '-1rem',
}}
>
{error.length > 0 &&
error.map((err, index) => (
<Message
icon="warning"
key={`${err}-${index}`}
negative
attached
header={this.props.intl.formatMessage(messages.error)}
content={err}
/>
))}
<DragDropContext onDragEnd={this.onDragEnd}>
<Droppable droppableId="tabs-schema-edit" direction="horizontal">
{(provided, snapshot) => (
<div
role="tablist"
className="ui pointing secondary attached tabular menu"
ref={provided.innerRef}
{...provided.draggableProps}
style={getTabStyle(snapshot.isDraggingOver)}
>
{this.props.value.fieldsets.map((fieldset, index) => (
<SchemaWidgetFieldset
key={`${fieldset.id}-${this.state.currentFieldset}-${index}`}
title={fieldset.title}
order={index}
active={index === this.state.currentFieldset}
onClick={this.onSetCurrentFieldset}
onShowEditFieldset={this.onShowEditFieldset}
onShowDeleteFieldset={this.onShowDeleteFieldset}
onOrderFieldset={this.onOrderFieldset}
getItemStyle={getItemStyle}
isDraggable={true}
isDisabled={
fieldset.behavior
? !fieldset.behavior.includes('generated')
: false
}
/>
))}
<div className="item item-add">
<button
aria-label={this.props.intl.formatMessage(messages.add)}
className="item ui noborder button"
onClick={this.onShowAddFieldset}
>
<Icon name="plus" size="large" />
</button>
</div>
{provided.placeholder}
</div>
)}
</Droppable>
{makeNonUserFields()}
<Droppable
droppableId="fields-schema-edit"
direction="vertical"
type="fixed"
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={getFieldStyle(snapshot.isDraggingOver)}
>
{makeUserFields()}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
{hasChangeNote ? (
<div style={{ background: '#c7d5d859' }}>
<Field
{...this.props.value.properties.changeNote}
id={'changeNote'}
required={
this.props.value.required.indexOf('changeNote') !== -1
}
onEdit={this.onShowEditField}
draggable={false}
isDisabled={true}
order={
this.props.value.fieldsets[this.state.currentFieldset]
.length - 1
}
onDelete={this.onShowDeleteField}
onChange={this.onChangeDefaultValue}
key={'changeNote'}
value={this.props.value.properties.changeNote.default}
/>
</div>
) : null}
{canAddFields && (
<Form.Field inline className="addfield">
<Grid>
<Grid.Row stretched>
<Grid.Column width="12">
<div className="wrapper">
<label htmlFor="addfield">
{this.props.intl.formatMessage(messages.addField)}
</label>
</div>
<div className="toolbar">
<button
aria-label={this.props.intl.formatMessage(messages.add)}
id="addfield"
className="item ui noborder button"
onClick={this.onShowAddField}
>
<Icon name="plus" color="blue" size="large" />
</button>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
</Form.Field>
)}
</Segment.Group>
{this.state.addField !== null && (
<ModalForm
onSubmit={this.onAddField}
onCancel={this.onCancel}
className={`field-${slugify(isString(this.state.addField) && this.state.addField !== '' ? this.state.addField : 'label_text_field')}`}
onChangeFormData={(data) => {
this.setState({
addField: data.factory,
});
}}
title={this.props.intl.formatMessage(messages.addField)}
formData={{
factory:
find(choices, { value: 'label_text_field' }) || undefined,
id,
}}
schema={schemaField(
this.state.addField,
this.props.intl,
this.props.value.fieldsets.filter(
(fieldset) =>
!fieldset.behavior ||
fieldset.id === 'default' ||
fieldset.behavior.includes('generated'),
),
this.props.allowEditId,
this.props.allowEditQueryParameter,
this.props.allowEditPlaceholder,
{
factory: {
type: 'string',
factory: 'Choice',
title: this.props.intl.formatMessage(messages.type),
vocabulary: {
'@id': `Fields`,
},
filterChoices: filterFactory,
additionalChoices: additionalFactory,
sort: true,
},
},
)}
/>
)}
{this.state.editField !== null && (
<ModalForm
onSubmit={this.onEditField}
onCancel={this.onCancel}
title={`${this.props.intl.formatMessage(messages.editField)}: ${editFieldType}`}
className={`factory-${slugify(editFieldType)}`}
formData={{
...this.props.value.properties[this.state.editField.id],
id: this.state.editField.id,
required:
this.props.value.required.indexOf(this.state.editField.id) !==
-1,
parentFieldSet:
this.props.value.fieldsets[this.state.currentFieldset].id,
values: formatArrayToTextarea(
this.props.value.properties[this.state.editField.id],
),
}}
schema={schemaField(
this.props.value.properties[this.state.editField.id].factory,
this.props.intl,
this.props.value.fieldsets.filter(
(fieldset) =>
!fieldset.behavior ||
fieldset.id === 'default' ||
fieldset.behavior.includes('generated'),
),
this.props.allowEditId,
this.props.allowEditQueryParameter,
this.props.allowEditPlaceholder,
)}
/>
)}
{this.state.addFieldset !== null && (
<ModalForm
onSubmit={this.onAddFieldset}
onCancel={this.onCancel}
title={this.props.intl.formatMessage(messages.addFieldset)}
formData={{
id: '',
t