Skip to content

Commit

Permalink
ENH Refactor FormBuilderModal and split up the Modal into its own com…
Browse files Browse the repository at this point in the history
…ponent (#1631)

* MNT Split out Modal out of FormBuilderModal

* MNT Refactor FormBuilderModal to be a functional component

* MNT Add test coverage for Modal component

* MNT Apply peer review feedback

* MNT Apply peer review feedback

* MNT Adress peer review feedback

* MNT Fold ModalHeader back into Modal
  • Loading branch information
Maxime Rainville authored Feb 26, 2024
1 parent cfd8e5e commit 41f8bd3
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 192 deletions.
44 changes: 22 additions & 22 deletions client/dist/js/bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/src/bundles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import 'expose-loader?exposes=ToastsActions!state/toasts/ToastsActions';
import 'expose-loader?exposes=FileStatusIcon!components/FileStatusIcon/FileStatusIcon';
import 'expose-loader?exposes=FormBuilder!components/FormBuilder/FormBuilder';
import 'expose-loader?exposes=FormBuilderLoader!containers/FormBuilderLoader/FormBuilderLoader';
import 'expose-loader?exposes=Modal!components/Modal/Modal';
import 'expose-loader?exposes=FormBuilderModal!components/FormBuilderModal/FormBuilderModal';
import 'expose-loader?exposes=FileSchemaModalHandler!containers/InsertLinkModal/fileSchemaModalHandler';
import 'expose-loader?exposes=InsertLinkModal!containers/InsertLinkModal/InsertLinkModal';
Expand Down
274 changes: 104 additions & 170 deletions client/src/components/FormBuilderModal/FormBuilderModal.js
Original file line number Diff line number Diff line change
@@ -1,104 +1,71 @@
import React, { Component } from 'react';
import i18n from 'i18n';
import { Modal, ModalHeader } from 'reactstrap';
import React, { useState } from 'react';
import FormBuilderLoader from 'containers/FormBuilderLoader/FormBuilderLoader';
import castStringToElement from 'lib/castStringToElement';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import Modal from 'components/Modal/Modal';

const noop = () => null;

class FormBuilderModal extends Component {
constructor(props) {
super(props);

this.handleSubmit = this.handleSubmit.bind(this);
this.handleHide = this.handleHide.bind(this);
this.clearResponse = this.clearResponse.bind(this);
this.handleLoadingError = this.handleLoadingError.bind(this);
}

/**
* Defines the form part of the Modal
*
* @returns {Component}
*/
getForm() {
const { FormBuilderLoaderComponent } = this.props;
if (!this.props.schemaUrl) {
return null;
}
return (
<FormBuilderLoaderComponent
fieldHolder={{ className: classnames('modal-body', this.props.bodyClassName) }}
actionHolder={{ className: 'modal-footer' }}
autoFocus={this.props.autoFocus}
schemaUrl={this.props.schemaUrl}
onSubmit={this.handleSubmit}
onAction={this.props.onAction}
onLoadingError={this.handleLoadingError}
identifier={this.props.identifier}
/>
);
}

/**
* Generates the response part of the Modal
*
* @returns {Component}
*/
getResponse() {
if (!this.state || !this.state.response) {
return null;
}

let className = '';

if (this.state.error) {
className = this.props.responseClassBad;
} else {
className = this.props.responseClassGood;
}

return (
<div className={className}>
{ castStringToElement('span', { html: this.state.response }) }
</div>
);
}

/**
* Removes the response from the state
*/
clearResponse() {
this.setState({
response: null,
});
}

handleLoadingError(schema) {
const providesOnLoadingError = this.props.onLoadingError !== noop;
if (this.props.showErrorMessage || !providesOnLoadingError) {
const error = schema.errors && schema.errors[0];
this.setState({
response: error.value,
error: true,
});
/**
* React component for displaying a Form in a Modal using a form schema URL
*/
const FormBuilderModal = ({
children,
FormBuilderLoaderComponent,
onLoadingError,
onSubmit,
responseClassBad,
responseClassGood,
showErrorMessage,

// Form builder props
autoFocus,
bodyClassName,
identifier,
onAction,
schemaUrl,

// Props pass to modal
className,
isOpen,
modalClassName,
ModalComponent,
ModalHeaderComponent,
onClosed,
showCloseButton,
size,
title,
}) => {
/** @var {string} response Message we got back from posting the form. */
const [response, setResponse] = useState(null);
/** @var {boolean} response Whether the response was an error or not. */
const [error, setError] = useState(null);

const handleLoadingError = (schema) => {
const providesOnLoadingError = onLoadingError !== noop;
if (showErrorMessage || !providesOnLoadingError) {
const errorResponse = schema.errors && schema.errors[0];
setResponse(errorResponse.value);
setError(true);
}
if (providesOnLoadingError) {
this.props.onLoadingError(schema);
onLoadingError(schema);
}
}
};

/**
* Call the callback for hiding this Modal
*/
handleHide() {
this.clearResponse();
if (typeof this.props.onClosed === 'function') {
this.props.onClosed();
const handleHide = () => {
// Clear state
setResponse(null);
setError(false);

if (typeof onClosed === 'function') {
onClosed();
}
}
};

/**
* Handle submitting the form in the Modal
Expand All @@ -108,107 +75,80 @@ class FormBuilderModal extends Component {
* @param {Function} submitFn The original submit function
* @returns {Promise}
*/
handleSubmit(data, action, submitFn) {
this.clearResponse();
const handleSubmit = (data, action, submitFn) => {
// Clear state
setResponse(null);
setError(false);

let promise = null;
if (typeof this.props.onSubmit === 'function') {
promise = this.props.onSubmit(data, action, submitFn);
if (typeof onSubmit === 'function') {
promise = onSubmit(data, action, submitFn);
} else {
promise = submitFn();
}

if (promise) {
// do not want this as part of the main promise chain.
promise
.then((response) => {
if (response) {
this.setState({
response: response.message,
error: false,
});
.then((successResponse) => {
if (successResponse) {
setResponse(successResponse.message);
setError(false);
}
return response;
return successResponse;
})
.catch((errorPromise) => {
errorPromise.then((errorText) => {
this.setState({
response: errorText,
error: true,
});
setResponse(errorText);
setError(true);
});
});
} else {
throw new Error('Promise was not returned for submitting');
}

return promise;
}

renderHeader() {
let { title } = this.props;
const { ModalHeaderComponent } = this.props;

if (title !== false) {
if (typeof title === 'object') {
// FormSchema title occasionaly contains html, only render text for modal title
const doc = new DOMParser().parseFromString(title.html, 'text/html');
title = doc.body.textContent || '';
};

const modalProps = {
className,
isOpen,
modalClassName,
ModalComponent,
ModalHeaderComponent,
onClosed: handleHide,
showCloseButton,
size,
title,
};
const formBuilderLoaderProps = {
actionHolder: { className: 'modal-footer' },
autoFocus,
bodyClassName,
fieldHolder: { className: classnames('modal-body', bodyClassName) },
identifier,
onAction,
onLoadingError: handleLoadingError,
onSubmit: handleSubmit,
schemaUrl,
};

return (
<Modal {...modalProps}>
{response &&
<div className={error ? responseClassBad : responseClassGood}>
{ castStringToElement('span', { html: response }) }
</div>
}
return (
<ModalHeaderComponent toggle={this.handleHide}>{title}</ModalHeaderComponent>
);
}

if (this.props.showCloseButton === true && typeof this.props.onClosed === 'function') {
return (
<button
type="button"
className="close modal__close-button"
onClick={this.handleHide}
aria-label={i18n._t('Admin.CLOSE', 'Close')}
/>
);
}

return null;
}

render() {
const form = this.getForm();
const response = this.getResponse();
const { ModalComponent } = this.props;

return (
<ModalComponent
isOpen={this.props.isOpen}
toggle={this.handleHide}
className={this.props.className}
modalClassName={this.props.modalClassName}
size={this.props.size}
>
{this.renderHeader()}
{response}
{form}
{this.props.children}
</ModalComponent>
);
}
}
{schemaUrl && <FormBuilderLoaderComponent {...formBuilderLoaderProps} />}
{children}
</Modal>
);
};

FormBuilderModal.propTypes = {
autoFocus: PropTypes.bool,
isOpen: PropTypes.bool,
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
PropTypes.shape({ html: PropTypes.string })
]),
className: PropTypes.string,
bodyClassName: PropTypes.string,
modalClassName: PropTypes.string,
showCloseButton: PropTypes.bool,
size: PropTypes.string,
onClosed: PropTypes.func,
schemaUrl: PropTypes.string,
onSubmit: PropTypes.func,
onAction: PropTypes.func,
Expand All @@ -218,22 +158,16 @@ FormBuilderModal.propTypes = {
// Ignored and assumed true if onLoadingError is unassigned
showErrorMessage: PropTypes.bool,
onLoadingError: PropTypes.func,
ModalComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
ModalHeaderComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
FormBuilderLoaderComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
...Modal.propTypes,
};

FormBuilderModal.defaultProps = {
showErrorMessage: false,
showCloseButton: true,
onLoadingError: noop,
isOpen: false,
title: null,
modalClassName: 'form-builder-modal',
responseClassGood: 'alert alert-success',
responseClassBad: 'alert alert-danger',
ModalComponent: Modal,
ModalHeaderComponent: ModalHeader,
FormBuilderLoaderComponent: FormBuilderLoader,
};

Expand Down
Loading

0 comments on commit 41f8bd3

Please sign in to comment.