Added support to define mosaico templates in MJML. (A wizard that shows how to do this is TODO.)

Adopted some core features (router, etc.) from IVIS.
This commit is contained in:
Tomas Bures 2019-04-03 12:13:49 +02:00
parent 3b20ac5ce7
commit ad9f5d16bf
28 changed files with 1381 additions and 538 deletions

View file

@ -2,45 +2,17 @@
import React, {Component} from "react";
import i18n, {withTranslation} from './i18n';
import PropTypes
from "prop-types";
import PropTypes from "prop-types";
import {withRouter} from "react-router";
import {
BrowserRouter as Router,
Link,
Redirect,
Route,
Switch
} from "react-router-dom";
import {
withAsyncErrorHandler,
withErrorHandling
} from "./error-handling";
import interoperableErrors
from "../../../shared/interoperable-errors";
import {
ActionLink,
Button,
DismissibleAlert,
DropdownActionLink,
Icon
} from "./bootstrap-components";
import mailtrainConfig
from "mailtrainConfig";
import styles
from "./styles.scss";
import {
getRoutes,
needsResolve,
resolve,
SectionContentContext,
withPageHelpers
} from "./page-common";
import {BrowserRouter as Router, Link, Route, Switch} from "react-router-dom";
import {withErrorHandling} from "./error-handling";
import interoperableErrors from "../../../shared/interoperable-errors";
import {ActionLink, Button, DismissibleAlert, DropdownActionLink, Icon} from "./bootstrap-components";
import mailtrainConfig from "mailtrainConfig";
import styles from "./styles.scss";
import {getRoutes, renderRoute, Resolver, SectionContentContext, withPageHelpers} from "./page-common";
import {getBaseDir} from "./urls";
import {
createComponentMixin,
withComponentMixins
} from "./decorator-helpers";
import {createComponentMixin, withComponentMixins} from "./decorator-helpers";
import {getLang} from "../../../shared/langs";
export { withPageHelpers }
@ -182,21 +154,81 @@ class TertiaryNavBar extends Component {
}
}
function getLoadingMessage(t) {
return (
<div className="container-fluid my-3">
{t('loading')}
</div>
);
}
function renderFrameWithContent(t, panelInFullScreen, showSidebar, primaryMenu, secondaryMenu, content) {
if (panelInFullScreen) {
return (
<div key="app" className="app panel-in-fullscreen">
<div key="appBody" className="app-body">
<main key="main" className="main">
{content}
</main>
</div>
</div>
);
} else {
return (
<div key="app" className={"app " + (showSidebar ? 'sidebar-lg-show' : '')}>
<header key="appHeader" className="app-header">
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
{showSidebar &&
<button className="navbar-toggler sidebar-toggler" data-toggle="sidebar-show" type="button">
<span className="navbar-toggler-icon"/>
</button>
}
<Link className="navbar-brand" to="/"><div><Icon icon="envelope"/> Mailtrain</div></Link>
<button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#mtMainNavbar" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"/>
</button>
<div className="collapse navbar-collapse" id="mtMainNavbar">
{primaryMenu}
</div>
</nav>
</header>
<div key="appBody" className="app-body">
{showSidebar &&
<div key="sidebar" className="sidebar">
{secondaryMenu}
</div>
}
<main key="main" className="main">
{content}
</main>
</div>
<footer key="appFooter" className="app-footer">
<div className="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGitHub')}</a></div>
</footer>
</div>
);
}
}
@withComponentMixins([
withTranslation,
withErrorHandling
withTranslation
])
class RouteContent extends Component {
class PanelRoute extends Component {
constructor(props) {
super(props);
this.state = {
panelInFullScreen: props.route.panelInFullScreen
};
if (Object.keys(props.route.resolve).length === 0) {
this.state.resolved = {};
}
this.sidebarAnimationNodeListener = evt => {
if (evt.propertyName === 'left') {
this.forceUpdate();
@ -208,33 +240,11 @@ class RouteContent extends Component {
static propTypes = {
route: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
flashMessage: PropTypes.object
}
@withAsyncErrorHandler
async resolve() {
const props = this.props;
if (Object.keys(props.route.resolve).length === 0) {
this.setState({
resolved: {}
});
} else {
this.setState({
resolved: null
});
const resolved = await resolve(props.route, props.match);
if (!this.disregardResolve) { // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
this.setState({
resolved
});
}
}
}
registerSidebarAnimationListener() {
if (this.sidebarAnimationNode) {
this.sidebarAnimationNode.addEventListener("transitionend", this.sidebarAnimationNodeListener);
@ -242,45 +252,23 @@ class RouteContent extends Component {
}
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.resolve();
this.registerSidebarAnimationListener();
}
componentDidUpdate(prevProps) {
this.registerSidebarAnimationListener();
if (this.props.location.state !== prevProps.location.state || (this.props.match.params !== prevProps.match.params && needsResolve(prevProps.route, this.props.route, prevProps.match, this.props.match))) {
// noinspection JSIgnoredPromiseFromCall
this.resolve();
}
}
componentWillUnmount() {
this.disregardResolve = true; // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
}
render() {
const t = this.props.t;
const route = this.props.route;
const params = this.props.match.params;
const resolved = this.state.resolved;
const showSidebar = !!route.secondaryMenuComponent;
const panelInFullScreen = this.state.panelInFullScreen;
if (!route.panelRender && !route.panelComponent && route.link) {
let link;
if (typeof route.link === 'function') {
link = route.link(params);
} else {
link = route.link;
}
return <Redirect to={link}/>;
} else {
const render = resolved => {
let primaryMenu = null;
let secondaryMenu = null;
let content = null;
@ -331,71 +319,21 @@ class RouteContent extends Component {
}
} else {
content = (
<div className="container-fluid my-3">
{t('loading')}
</div>
);
content = getLoadingMessage(t);
}
if (panelInFullScreen) {
return (
<div key="app" className="app panel-in-fullscreen">
<div key="appBody" className="app-body">
<main key="main" className="main">
{content}
</main>
</div>
</div>
);
return renderFrameWithContent(t, panelInFullScreen, showSidebar, primaryMenu, secondaryMenu, content);
};
} else {
return (
<div key="app" className={"app " + (showSidebar ? 'sidebar-lg-show' : '')}>
<header key="appHeader" className="app-header">
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
{showSidebar &&
<button className="navbar-toggler sidebar-toggler" data-toggle="sidebar-show" type="button">
<span className="navbar-toggler-icon"/>
</button>
}
<Link className="navbar-brand" to="/"><div><Icon icon="envelope"/> Mailtrain</div></Link>
<button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#mtMainNavbar" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"/>
</button>
<div className="collapse navbar-collapse" id="mtMainNavbar">
{primaryMenu}
</div>
</nav>
</header>
<div key="appBody" className="app-body">
{showSidebar &&
<div key="sidebar" className="sidebar">
{secondaryMenu}
</div>
}
<main key="main" className="main">
{content}
</main>
</div>
<footer key="appFooter" className="app-footer">
<div className="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGitHub')}</a></div>
</footer>
</div>
);
}
}
return <Resolver route={route} render={render} location={this.props.location} match={this.props.match}/>;
}
}
@withRouter
@withComponentMixins([
withTranslation,
withErrorHandling
])
export class SectionContent extends Component {
@ -465,18 +403,28 @@ export class SectionContent extends Component {
}
renderRoute(route) {
let flashMessage;
if (this.state.flashMessageText) {
flashMessage = <DismissibleAlert severity={this.state.flashMessageSeverity} onCloseAsync={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>;
}
const t = this.props.t;
const render = props => <RouteContent route={route} flashMessage={flashMessage} {...props}/>;
const render = props => {
let flashMessage;
if (this.state.flashMessageText) {
flashMessage = <DismissibleAlert severity={this.state.flashMessageSeverity} onCloseAsync={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>;
}
return <Route key={route.path} exact path={route.path} render={render} />
return renderRoute(
route,
PanelRoute,
() => renderFrameWithContent(t,false, false, null, null, getLoadingMessage(this.props.t)),
flashMessage,
props
);
};
return <Route key={route.path} exact={route.exact} path={route.path} render={render} />
}
render() {
let routes = getRoutes('', {}, [], this.props.structure, [], null, null);
const routes = getRoutes(this.props.structure);
return (
<SectionContentContext.Provider value={this}>