@plone/volto
Version:
Volto
519 lines (483 loc) • 16.6 kB
JSX
/**
* Add container.
* @module components/manage/Add/Add
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'redux';
import keys from 'lodash/keys';
import isEmpty from 'lodash/isEmpty';
import { defineMessages, injectIntl } from 'react-intl';
import { Button, Grid, Menu } from 'semantic-ui-react';
import { createPortal } from 'react-dom';
import { v4 as uuid } from 'uuid';
import qs from 'query-string';
import { toast } from 'react-toastify';
import { createContent } from '@plone/volto/actions/content/content';
import { getSchema } from '@plone/volto/actions/schema/schema';
import { changeLanguage } from '@plone/volto/actions/language/language';
import { setFormData } from '@plone/volto/actions/form/form';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
import Sidebar from '@plone/volto/components/manage/Sidebar/Sidebar';
import Toast from '@plone/volto/components/manage/Toast/Toast';
import TranslationObject from '@plone/volto/components/manage/Multilingual/TranslationObject';
import { Form } from '@plone/volto/components/manage/Form';
import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url';
import {
hasBlocksData,
getBlocksFieldname,
getBlocksLayoutFieldname,
} from '@plone/volto/helpers/Blocks/Blocks';
import { getLanguageIndependentFields } from '@plone/volto/helpers/Content/Content';
import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
import { toGettextLang } from '@plone/volto/helpers/Utils/Utils';
import {
getSimpleDefaultBlocks,
getDefaultBlocks,
} from '@plone/volto/helpers/Blocks/defaultBlocks';
import {
tryParseJSON,
extractInvariantErrors,
} from '@plone/volto/helpers/FormValidation/FormValidation';
import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
import { preloadLazyLibs } from '@plone/volto/helpers/Loadable';
import config from '@plone/volto/registry';
import saveSVG from '@plone/volto/icons/save.svg';
import clearSVG from '@plone/volto/icons/clear.svg';
const messages = defineMessages({
add: {
id: 'Add {type}',
defaultMessage: 'Add {type}',
},
save: {
id: 'Save',
defaultMessage: 'Save',
},
cancel: {
id: 'Cancel',
defaultMessage: 'Cancel',
},
error: {
id: 'Error',
defaultMessage: 'Error',
},
translateTo: {
id: 'Translate to {lang}',
defaultMessage: 'Translate to {lang}',
},
someErrors: {
id: 'There are some errors.',
defaultMessage: 'There are some errors.',
},
});
/**
* Add class.
* @class Add
* @extends Component
*/
class Add extends Component {
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
static propTypes = {
createContent: PropTypes.func.isRequired,
getSchema: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
schema: PropTypes.objectOf(PropTypes.any),
content: PropTypes.shape({
// eslint-disable-line react/no-unused-prop-types
'@id': PropTypes.string,
'@type': PropTypes.string,
}),
returnUrl: PropTypes.string,
createRequest: PropTypes.shape({
loading: PropTypes.bool,
loaded: PropTypes.bool,
}).isRequired,
schemaRequest: PropTypes.shape({
loading: PropTypes.bool,
loaded: PropTypes.bool,
}).isRequired,
type: PropTypes.string,
location: PropTypes.objectOf(PropTypes.any),
};
/**
* Default properties
* @property {Object} defaultProps Default properties.
* @static
*/
static defaultProps = {
schema: null,
content: null,
returnUrl: null,
type: 'Default',
};
/**
* Constructor
* @method constructor
* @param {Object} props Component properties
* @constructs WysiwygEditor
*/
constructor(props) {
super(props);
this.onCancel = this.onCancel.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.state = {
isClient: false,
error: null,
formSelected: 'addForm',
};
}
/**
* Component did mount
* @method componentDidMount
* @returns {undefined}
*/
componentDidMount() {
this.props.getSchema(this.props.type, getBaseUrl(this.props.pathname));
this.setState({ isClient: true });
}
/**
* Component will receive props
* @method componentWillReceiveProps
* @param {Object} nextProps Next properties
* @returns {undefined}
*/
UNSAFE_componentWillReceiveProps(nextProps) {
if (
this.props.createRequest.loading &&
nextProps.createRequest.loaded &&
nextProps.content['@type'] === this.props.type
) {
this.props.setFormData({});
this.props.history.push(
this.props.returnUrl || flattenToAppURL(nextProps.content['@id']),
);
}
if (this.props.createRequest.loading && nextProps.createRequest.error) {
const message =
nextProps.createRequest.error.response?.body?.message ||
nextProps.createRequest.error.response?.text;
const error =
new DOMParser().parseFromString(message, 'text/html')?.all[0]
?.textContent || message;
const errorsList = tryParseJSON(error);
let erroMessage;
if (Array.isArray(errorsList)) {
const invariantErrors = extractInvariantErrors(errorsList);
if (invariantErrors.length > 0) {
// Plone invariant validation message.
erroMessage = invariantErrors.join(' - ');
} else {
// Error in specific field.
erroMessage = this.props.intl.formatMessage(messages.someErrors);
}
} else {
erroMessage = errorsList.error?.message || error;
}
this.setState({ error: error });
toast.error(
<Toast
error
title={this.props.intl.formatMessage(messages.error)}
content={erroMessage}
/>,
);
}
}
/**
* Submit handler
* @method onSubmit
* @param {object} data Form data.
* @returns {undefined}
*/
onSubmit(data) {
this.props.createContent(getBaseUrl(this.props.pathname), {
...data,
'@static_behaviors': this.props.schema.definitions
? keys(this.props.schema.definitions)
: null,
'@type': this.props.type,
...(config.settings.isMultilingual &&
this.props.location?.state?.translationOf && {
translation_of: this.props.location.state.translationOf,
language: this.props.location.state.language,
}),
});
}
/**
* Cancel handler
* @method onCancel
* @returns {undefined}
*/
onCancel() {
this.props.setFormData({});
if (this.props.location?.state?.translationOf) {
const language = this.props.location.state.languageFrom;
const langFileName = toGettextLang(language);
import(
/* @vite-ignore */ '@root/../locales/' + langFileName + '.json'
).then((locale) => {
this.props.changeLanguage(language, locale.default);
});
this.props.history.push(this.props.location?.state?.translationOf);
} else {
this.props.history.push(getBaseUrl(this.props.pathname));
}
}
form = React.createRef();
/**
* Render method.
* @method render
* @returns {string} Markup for the component.
*/
render() {
if (this.props.schemaRequest.loaded) {
const visual = hasBlocksData(this.props.schema.properties);
const blocksFieldname = getBlocksFieldname(this.props.schema.properties);
const blocksLayoutFieldname = getBlocksLayoutFieldname(
this.props.schema.properties,
);
const translationObject = this.props.location?.state?.translationObject;
const translateTo = translationObject
? langmap?.[this.props.location?.state?.language]?.nativeName
: null;
// Get initial blocks from local config, if any
let initialBlocks, initialBlocksLayout;
const initialContentTypeBlocks =
config.blocks?.initialBlocks[this.props.type];
if (initialContentTypeBlocks) {
if (typeof initialContentTypeBlocks?.[0] === 'string') {
// Simple (legacy) default blocks definition
[initialBlocks, initialBlocksLayout] = getSimpleDefaultBlocks(
initialContentTypeBlocks,
);
} else {
[initialBlocks, initialBlocksLayout] = getDefaultBlocks(
initialContentTypeBlocks,
);
}
}
// Lookup initialBlocks and initialBlocksLayout within schema, if any
const schemaBlocks =
this.props.schema.properties[blocksFieldname]?.default;
const schemaBlocksLayout =
this.props.schema.properties[blocksLayoutFieldname]?.default?.items;
if (!isEmpty(schemaBlocksLayout) && !isEmpty(schemaBlocks)) {
initialBlocks = {};
initialBlocksLayout = [];
schemaBlocksLayout.forEach((value) => {
if (!isEmpty(schemaBlocks[value])) {
let newUid = uuid();
initialBlocksLayout.push(newUid);
initialBlocks[newUid] = schemaBlocks[value];
initialBlocks[newUid].block = newUid;
// Layout ID - keep a reference to the original block id within layout
initialBlocks[newUid]['@layout'] = value;
}
});
}
//copy blocks from translationObject
if (translationObject && blocksFieldname && blocksLayoutFieldname) {
initialBlocks = {};
initialBlocksLayout = [];
const originalBlocks = JSON.parse(
JSON.stringify(translationObject[blocksFieldname]),
);
const originalBlocksLayout =
translationObject[blocksLayoutFieldname].items;
originalBlocksLayout.forEach((value) => {
if (!isEmpty(originalBlocks[value])) {
let newUid = uuid();
initialBlocksLayout.push(newUid);
initialBlocks[newUid] = originalBlocks[value];
initialBlocks[newUid].block = newUid;
// Layout ID - keep a reference to the original block id within layout
initialBlocks[newUid]['@canonical'] = value;
}
});
}
const lifData = () => {
const data = {};
if (translationObject) {
getLanguageIndependentFields(this.props.schema).forEach(
(lif) => (data[lif] = translationObject[lif]),
);
}
return data;
};
const pageAdd = (
<div id="page-add">
<Helmet
title={this.props.intl.formatMessage(messages.add, {
type: this.props?.schema?.title || this.props.type,
})}
/>
<Form
ref={this.form}
key="translated-or-new-content-form"
navRoot={
this.props.content?.['@components']?.navroot?.navroot || {}
}
schema={this.props.schema}
type={this.props.type}
formData={
this.props.location?.state?.initialFormData || {
...(blocksFieldname && {
[blocksFieldname]:
initialBlocks ||
this.props.schema.properties[blocksFieldname]?.default,
}),
...(blocksLayoutFieldname && {
[blocksLayoutFieldname]: {
items:
initialBlocksLayout ||
this.props.schema.properties[blocksLayoutFieldname]
?.default?.items,
},
}),
// Copy the Language Independent Fields values from the to-be translated content
// into the default values of the translated content Add form.
...lifData(),
parent: {
'@id': this.props.content?.['@id'] || '',
},
}
}
requestError={this.state.error}
onSubmit={this.onSubmit}
hideActions
pathname={this.props.pathname}
visual={visual}
title={
this.props?.schema?.title
? this.props.intl.formatMessage(messages.add, {
type: this.props.schema.title,
})
: null
}
loading={this.props.createRequest.loading}
isFormSelected={this.state.formSelected === 'addForm'}
onSelectForm={() => {
this.setState({ formSelected: 'addForm' });
}}
global
// Properties to pass to the BlocksForm to match the View ones
history={this.props.history}
location={this.props.location}
token={this.props.token}
/>
{this.state.isClient &&
createPortal(
<Toolbar
pathname={this.props.pathname}
hideDefaultViewButtons
inner={
<>
<Button
id="toolbar-save"
className="save"
aria-label={this.props.intl.formatMessage(messages.save)}
onClick={() => this.form.current.onSubmit()}
loading={this.props.createRequest.loading}
disabled={this.props.createRequest.loading}
>
<Icon
name={saveSVG}
className="circled"
size="30px"
title={this.props.intl.formatMessage(messages.save)}
/>
</Button>
<Button
className="cancel"
onClick={() => this.onCancel()}
type="button"
>
<Icon
name={clearSVG}
className="circled"
aria-label={this.props.intl.formatMessage(
messages.cancel,
)}
size="30px"
title={this.props.intl.formatMessage(messages.cancel)}
/>
</Button>
</>
}
/>,
document.getElementById('toolbar'),
)}
{visual &&
this.state.isClient &&
createPortal(<Sidebar />, document.getElementById('sidebar'))}
</div>
);
return translationObject ? (
<>
<BodyClass className="babel-view" />
<Grid
celled="internally"
stackable
columns={2}
id="page-add-translation"
>
<Grid.Column className="source-object">
<TranslationObject
translationObject={translationObject}
schema={this.props.schema}
pathname={this.props.pathname}
visual={visual}
isFormSelected={
this.state.formSelected === 'translationObjectForm'
}
onSelectForm={() => {
this.setState({
formSelected: 'translationObjectForm',
});
}}
/>
</Grid.Column>
<Grid.Column>
<div className="new-translation">
<Menu pointing secondary attached tabular>
<Menu.Item name={translateTo.toUpperCase()} active={true}>
{`${this.props.intl.formatMessage(messages.translateTo, {
lang: translateTo,
})}`}
</Menu.Item>
</Menu>
{pageAdd}
</div>
</Grid.Column>
</Grid>
</>
) : (
pageAdd
);
}
return <div />;
}
}
export default compose(
injectIntl,
connect(
(state, props) => ({
createRequest: state.content.create,
schemaRequest: state.schema,
content: state.content.data,
schema: state.schema.schema,
pathname: props.location.pathname,
returnUrl: qs.parse(props.location.search).return_url,
type: qs.parse(props.location.search).type,
}),
{ createContent, getSchema, changeLanguage, setFormData },
),
preloadLazyLibs('cms'),
)(Add);