mailtrain/client/src/lib/modals.js
Tomas Bures 48dcf2c701 Mosaico upgraded to 0.17.5
Work started on confirmation dialogs displayed when one navigates from a page with unsaved changes
2019-05-08 19:54:19 +02:00

373 lines
12 KiB
JavaScript

'use strict';
import React, {Component} from 'react';
import axios, {HTTPMethod} from './axios';
import {withTranslation} from './i18n';
import PropTypes from 'prop-types';
import {Icon, ModalDialog} from "./bootstrap-components";
import {getUrl} from "./urls";
import {withPageHelpers} from "./page";
import styles from './styles.scss';
import interoperableErrors from '../../../shared/interoperable-errors';
import {Link} from "react-router-dom";
import {withComponentMixins} from "./decorator-helpers";
import {withAsyncErrorHandler} from "./error-handling";
import ACEEditorRaw from 'react-ace';
@withComponentMixins([
withTranslation,
withPageHelpers
])
export class RestActionModalDialog extends Component {
static propTypes = {
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
stateOwner: PropTypes.object,
visible: PropTypes.bool.isRequired,
actionMethod: PropTypes.func.isRequired,
actionUrl: PropTypes.string.isRequired,
actionData: PropTypes.object,
backUrl: PropTypes.string,
successUrl: PropTypes.string,
onBack: PropTypes.func,
onPerformingAction: PropTypes.func,
onSuccess: PropTypes.func,
actionInProgressMsg: PropTypes.string.isRequired,
actionDoneMsg: PropTypes.string.isRequired,
onErrorAsync: PropTypes.func
}
async hideModal(isBack) {
if (this.props.backUrl) {
this.navigateTo(this.props.backUrl);
} else {
if (isBack) {
this.props.onBack();
} else {
this.props.onPerformingAction();
}
}
}
async performAction() {
const props = this.props;
const t = props.t;
const owner = props.stateOwner;
await this.hideModal(false);
try {
if (!owner) {
this.setFlashMessage('info', props.actionInProgressMsg);
} else {
owner.disableForm();
owner.setFormStatusMessage('info', props.actionInProgressMsg);
}
await axios.method(props.actionMethod, getUrl(props.actionUrl), props.actionData);
if (props.successUrl) {
this.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
} else {
props.onSuccess();
this.setFlashMessage('success', props.actionDoneMsg);
}
} catch (err) {
if (props.onErrorAsync) {
await props.onErrorAsync(err);
} else {
throw err;
}
}
}
render() {
const t = this.props.t;
return (
<ModalDialog hidden={!this.props.visible} title={this.props.title} onCloseAsync={() => this.hideModal(true)} buttons={[
{ label: t('no'), className: 'btn-primary', onClickAsync: async () => await this.hideModal(true) },
{ label: t('yes'), className: 'btn-danger', onClickAsync: ::this.performAction }
]}>
{this.props.message}
</ModalDialog>
);
}
}
const entityTypeLabels = {
'namespace': t => t('namespace'),
'list': t => t('list'),
'customForm': t => t('customForms'),
'campaign': t => t('campaign'),
'template': t => t('template'),
'sendConfiguration': t => t('sendConfiguration'),
'report': t => t('report'),
'reportTemplate': t => t('reportTemplate'),
'mosaicoTemplate': t => t('mosaicoTemplate')
};
function _getDependencyErrorMessage(err, t, name) {
return (
<div>
<p>{t('cannoteDeleteNameDueToTheFollowing', {name})}</p>
<ul className={styles.errorsList}>
{err.data.dependencies.map(dep =>
dep.link ?
<li key={dep.link}><Link to={dep.link}>{entityTypeLabels[dep.entityTypeId](t)}: {dep.name}</Link></li>
: // if no dep.link is present, it means the user has no permission to view the entity, thus only id without the link is shown
<li key={dep.id}>{entityTypeLabels[dep.entityTypeId](t)}: [{dep.id}]</li>
)}
{err.data.andMore && <li>{t('andMore')}</li>}
</ul>
</div>
);
}
@withComponentMixins([
withTranslation,
withPageHelpers
])
export class DeleteModalDialog extends Component {
constructor(props) {
super(props);
const t = props.t;
}
static propTypes = {
visible: PropTypes.bool.isRequired,
stateOwner: PropTypes.object.isRequired,
deleteUrl: PropTypes.string.isRequired,
backUrl: PropTypes.string,
successUrl: PropTypes.string,
deletingMsg: PropTypes.string.isRequired,
deletedMsg: PropTypes.string.isRequired
}
async onErrorAsync(err) {
const t = this.props.t;
if (err instanceof interoperableErrors.DependencyPresentError) {
const owner = this.props.stateOwner;
const name = owner.getFormValue('name');
this.setFlashMessage('danger', _getDependencyErrorMessage(err, t, name));
window.scrollTo(0, 0); // This is to scroll up because the flash message appears on top and it's quite misleading if the delete fails and the message is not in the viewport
owner.enableForm();
owner.clearFormStatusMessage();
} else {
throw err;
}
}
render() {
const t = this.props.t;
const owner = this.props.stateOwner;
const name = owner.getFormValue('name') || '';
return <RestActionModalDialog
title={t('confirmDeletion')}
message={t('areYouSureYouWantToDeleteName?', {name})}
stateOwner={this.props.stateOwner}
visible={this.props.visible}
actionMethod={HTTPMethod.DELETE}
actionUrl={this.props.deleteUrl}
backUrl={this.props.backUrl}
successUrl={this.props.successUrl}
actionInProgressMsg={this.props.deletingMsg}
actionDoneMsg={this.props.deletedMsg}
onErrorAsync={::this.onErrorAsync}
/>
}
}
export function tableRestActionDialogInit(owner) {
owner.tableRestActionDialogData = {};
owner.state.tableRestActionDialogShown = false;
}
function _hide(owner, dontRefresh = false) {
owner.tableRestActionDialogData = {};
owner.setState({ tableRestActionDialogShown: false });
if (!dontRefresh) {
owner.table.refresh();
}
}
export function tableAddDeleteButton(actions, owner, perms, deleteUrl, name, deletingMsg, deletedMsg) {
const t = owner.props.t;
async function onErrorAsync(err) {
if (err instanceof interoperableErrors.DependencyPresentError) {
owner.setFlashMessage('danger', _getDependencyErrorMessage(err, t, name));
window.scrollTo(0, 0); // This is to scroll up because the flash message appears on top and it's quite misleading if the delete fails and the message is not in the viewport
_hide(owner);
} else {
throw err;
}
}
if (!perms || perms.includes('delete')) {
if (owner.tableRestActionDialogData.shown) {
actions.push({
label: <Icon className={styles.iconDisabled} icon="trash-alt" title={t('delete')}/>
});
} else {
actions.push({
label: <Icon icon="trash-alt" title={t('delete')}/>,
action: () => {
owner.tableRestActionDialogData = {
shown: true,
title: t('confirmDeletion'),
message:t('areYouSureYouWantToDeleteName?', {name}),
httpMethod: HTTPMethod.DELETE,
actionUrl: deleteUrl,
actionInProgressMsg: deletingMsg,
actionDoneMsg: deletedMsg,
onErrorAsync: onErrorAsync
};
owner.setState({
tableRestActionDialogShown: true
});
owner.table.refresh();
}
});
}
}
}
export function tableAddRestActionButton(actions, owner, action, button, title, message, actionInProgressMsg, actionDoneMsg, onErrorAsync) {
const t = owner.props.t;
if (owner.tableRestActionDialogData.shown) {
actions.push({
label: <Icon className={styles.iconDisabled} icon={button.icon} title={button.label}/>
});
} else {
actions.push({
label: <Icon icon={button.icon} title={button.label}/>,
action: () => {
owner.tableRestActionDialogData = {
shown: true,
title: title,
message: message,
httpMethod: action.method,
actionUrl: action.url,
actionData: action.data,
actionInProgressMsg: actionInProgressMsg,
actionDoneMsg: actionDoneMsg,
onErrorAsync: onErrorAsync
};
owner.setState({
tableRestActionDialogShown: true
});
owner.table.refresh();
}
});
}
}
export function tableRestActionDialogRender(owner) {
const data = owner.tableRestActionDialogData;
return <RestActionModalDialog
title={data.title || ''}
message={data.message || ''}
visible={owner.state.tableRestActionDialogShown}
actionMethod={data.httpMethod || HTTPMethod.POST}
actionUrl={data.actionUrl || ''}
actionData={data.actionData}
onBack={() => _hide(owner)}
onPerformingAction={() => _hide(owner, true)}
onSuccess={() => _hide(owner)}
actionInProgressMsg={data.actionInProgressMsg || ''}
actionDoneMsg={data.actionDoneMsg || ''}
onErrorAsync={data.onErrorAsync}
/>
}
@withComponentMixins([
withTranslation
])
export class ContentModalDialog extends Component {
constructor(props) {
super(props);
const t = props.t;
this.state = {
content: null
};
}
static propTypes = {
visible: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
getContentAsync: PropTypes.func.isRequired,
onHide: PropTypes.func.isRequired
}
@withAsyncErrorHandler
async fetchContent() {
const content = await this.props.getContentAsync();
this.setState({
content
});
}
componentDidMount() {
if (this.props.visible) {
// noinspection JSIgnoredPromiseFromCall
this.fetchContent();
}
}
componentDidUpdate(prevProps) {
if (this.props.visible && !prevProps.visible) {
// noinspection JSIgnoredPromiseFromCall
this.fetchContent();
} else if (!this.props.visible && this.state.content !== null) {
this.setState({
content: null
});
}
}
render() {
const t = this.props.t;
return (
<ModalDialog hidden={!this.props.visible} title={this.props.title} onCloseAsync={() => this.props.onHide()}>
{this.props.visible && this.state.content &&
<ACEEditorRaw
mode='xml'
theme="github"
fontSize={12}
width="100%"
height="600px"
showPrintMargin={false}
value={this.state.content}
tabSize={2}
setOptions={{useWorker: false}} // This disables syntax check because it does not always work well (e.g. in case of JS code in report templates)
readOnly={true}
/>
}
</ModalDialog>
);
}
}