Some preparations for activity log.

Fixed issue #524
Table now displays horizontal scrollbar when the viewport is too narrow (typically on mobile)
This commit is contained in:
Tomas Bures 2019-02-07 14:38:32 +00:00
parent 4f408a26d5
commit e0bee9ed42
28 changed files with 353 additions and 97 deletions

View file

@ -67,7 +67,8 @@ export default class CustomContent extends Component {
}
static propTypes = {
entity: PropTypes.object
entity: PropTypes.object,
setPanelInFullScreen: PropTypes.func
}
loadFromEntityMutator(data) {
@ -177,6 +178,7 @@ export default class CustomContent extends Component {
}
async setElementInFullscreen(elementInFullscreen) {
this.props.setPanelInFullScreen(elementInFullscreen);
this.setState({
elementInFullscreen
});

View file

@ -33,6 +33,7 @@ import StatisticsOpened
from "./StatisticsOpened";
import StatisticsLinkClicks
from "./StatisticsLinkClicks";
import TemplatesCUD from "../templates/root";
function getMenus(t) {
@ -120,7 +121,7 @@ function getMenus(t) {
campaignContent: params => `rest/campaigns-content/${params.campaignId}`
},
visible: resolved => resolved.campaign.permissions.includes('edit') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
panelRender: props => <Content entity={props.resolved.campaignContent} />
panelRender: props => <Content entity={props.resolved.campaignContent} setPanelInFullScreen={props.setPanelInFullScreen} />
},
files: {
title: t('files'),

View file

@ -74,7 +74,8 @@ export class Button extends Component {
iconTitle: PropTypes.string,
className: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string
type: PropTypes.string,
disabled: PropTypes.bool
}
@withAsyncErrorHandler
@ -106,7 +107,7 @@ export class Button extends Component {
}
return (
<button type={type} className={className} onClick={::this.onClick} title={this.props.title}>{icon}{iconSpacer}{props.label}</button>
<button type={type} className={className} onClick={::this.onClick} title={this.props.title} disabled={this.props.disabled}>{icon}{iconSpacer}{props.label}</button>
);
}
}
@ -301,7 +302,7 @@ export class ModalDialog extends Component {
const buttons = [];
for (let idx = 0; idx < this.state.buttons.length; idx++) {
const buttonSpec = this.state.buttons[idx];
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={() => this.onButtonClick(idx)} />
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={async () => this.onButtonClick(idx)} />
buttons.push(button);
}

View file

@ -54,8 +54,8 @@ i18n
whitelist: mailtrainConfig.enabledLanguages,
load: 'currentOnly',
debug: true
})
debug: false
});
export default i18n;
@ -64,7 +64,7 @@ export default i18n;
export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => {
return {
cls: withNamespaces()(TargetClass)
}
};
});
export function tMark(key) {

View file

@ -94,7 +94,7 @@ export class RestActionModalDialog extends Component {
return (
<ModalDialog hidden={!this.props.visible} title={this.props.title} onCloseAsync={() => this.hideModal(true)} buttons={[
{ label: t('no'), className: 'btn-primary', onClickAsync: () => this.hideModal(true) },
{ label: t('no'), className: 'btn-primary', onClickAsync: async () => this.hideModal(true) },
{ label: t('yes'), className: 'btn-danger', onClickAsync: ::this.performAction }
]}>
{this.props.message}

View file

@ -92,6 +92,8 @@ export function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryM
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
title: entry.title,
link: entry.link,
panelInFullScreen: entry.panelInFullScreen,
insideIframe: entry.insideIframe,
resolve: entryResolve,
parents,
navs: [...navs, ...entryNavs]

View file

@ -189,7 +189,9 @@ class TertiaryNavBar extends Component {
class RouteContent extends Component {
constructor(props) {
super(props);
this.state = {};
this.state = {
panelInFullScreen: props.route.panelInFullScreen
};
if (Object.keys(props.route.resolve).length === 0) {
this.state.resolved = {};
@ -200,6 +202,8 @@ class RouteContent extends Component {
this.forceUpdate();
}
};
this.setPanelInFullScreen = panelInFullScreen => this.setState({ panelInFullScreen });
}
static propTypes = {
@ -208,7 +212,9 @@ class RouteContent extends Component {
}
@withAsyncErrorHandler
async resolve(props) {
async resolve() {
const props = this.props;
if (Object.keys(props.route.resolve).length === 0) {
this.setState({
resolved: {}
@ -237,18 +243,16 @@ class RouteContent extends Component {
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.resolve(this.props);
this.resolve();
this.registerSidebarAnimationListener();
}
componentDidUpdate() {
componentDidUpdate(prevProps) {
this.registerSidebarAnimationListener();
}
componentWillReceiveProps(nextProps) {
if (this.props.match.params !== nextProps.match.params && needsResolve(this.props.route, nextProps.route, this.props.match, nextProps.match)) {
if (this.props.match.params !== prevProps.match.params && needsResolve(prevProps.route, this.props.route, prevProps.match, this.props.match)) {
// noinspection JSIgnoredPromiseFromCall
this.resolve(nextProps);
this.resolve();
}
}
@ -264,6 +268,8 @@ class RouteContent extends Component {
const showSidebar = !!route.secondaryMenuComponent;
const panelInFullScreen = this.state.panelInFullScreen;
if (!route.panelRender && !route.panelComponent && route.link) {
let link;
if (typeof route.link === 'function') {
@ -283,7 +289,9 @@ class RouteContent extends Component {
const compProps = {
match: this.props.match,
location: this.props.location,
resolved
resolved,
setPanelInFullScreen: this.setPanelInFullScreen,
panelInFullScreen: this.state.panelInFullScreen
};
let panel;
@ -301,20 +309,27 @@ class RouteContent extends Component {
secondaryMenu = React.createElement(route.secondaryMenuComponent, compProps);
}
content = (
<>
<div className="mt-breadcrumb-and-tertiary-navbar">
<Breadcrumb route={route} params={params} resolved={resolved}/>
<TertiaryNavBar route={route} params={params} resolved={resolved}/>
</div>
<div className="container-fluid">
{this.props.flashMessage}
{panel}
</div>
</>
const panelContent = (
<div key="panel" className="container-fluid">
{this.props.flashMessage}
{panel}
</div>
);
if (panelInFullScreen) {
content = panelContent;
} else {
content = (
<>
<div key="tertiaryNav" className="mt-breadcrumb-and-tertiary-navbar">
<Breadcrumb route={route} params={params} resolved={resolved}/>
<TertiaryNavBar route={route} params={params} resolved={resolved}/>
</div>
{panelContent}
</>
);
}
} else {
content = (
<div className="container-fluid">
@ -323,45 +338,57 @@ class RouteContent extends Component {
);
}
return (
<div className={"app " + (showSidebar ? 'sidebar-lg-show' : '')}>
<header 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 className="app-body">
{showSidebar &&
<div className="sidebar">
{secondaryMenu}
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>
}
<main className="main">
{content}
</main>
</div>
);
<footer 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>
);
} 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>
);
}
}
}
}
@ -656,7 +683,7 @@ export function getLanguageChooser(t) {
const label = langDesc.getLabel(t);
languageOptions.push(
<DropdownActionLink key={lng} onClickAsync={() => i18n.changeLanguage(langDesc.longCode)}>{label}</DropdownActionLink>
<DropdownActionLink key={lng} onClickAsync={async () => i18n.changeLanguage(langDesc.longCode)}>{label}</DropdownActionLink>
)
}

View file

@ -2,6 +2,7 @@
.toolbar {
float: right;
margin-bottom: 15px;
}
.form { // This is here to give the styles below higher priority than Bootstrap has
@ -60,6 +61,10 @@
padding-bottom: 5px;
}
.dataTableTable {
overflow-x: auto;
}
.actionLinks > * {
margin-right: 8px;
}

View file

@ -276,7 +276,11 @@ class Table extends Component {
const dtOptions = {
columns,
pageLength: this.props.pageLength
pageLength: this.props.pageLength,
dom: // This overrides Bootstrap 4 settings. It may need to be updated if there are updates in the DataTables Bootstrap 4 plugin.
"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
"<'row'<'col-sm-12'<'" + styles.dataTableTable + "'tr>>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>"
};
const self = this;

View file

@ -65,8 +65,8 @@ class TreeTable extends Component {
}
@withAsyncErrorHandler
async loadData(dataUrl) {
const response = await axios.get(getUrl(dataUrl));
async loadData() {
const response = await axios.get(getUrl(this.props.dataUrl));
const treeData = response.data;
for (const root of treeData) {
@ -95,17 +95,6 @@ class TreeTable extends Component {
className: PropTypes.string
}
componentWillReceiveProps(nextProps) {
if (nextProps.data) {
this.setState({
treeData: nextProps.data
});
} else if (nextProps.dataUrl && this.props.dataUrl !== nextProps.dataUrl) {
// noinspection JSIgnoredPromiseFromCall
this.loadData(next.props.dataUrl);
}
}
shouldComponentUpdate(nextProps, nextState) {
return this.props.selection !== nextProps.selection || this.state.treeData != nextState.treeData || this.props.className !== nextProps.className;
}
@ -129,7 +118,7 @@ class TreeTable extends Component {
componentDidMount() {
if (!this.props.data && this.props.dataUrl) {
// noinspection JSIgnoredPromiseFromCall
this.loadData(this.props.dataUrl);
this.loadData();
}
let createNodeFn;
@ -221,6 +210,15 @@ class TreeTable extends Component {
}
componentDidUpdate(prevProps, prevState) {
if (this.props.data) {
this.setState({
treeData: this.props.data
});
} else if (this.props.dataUrl && prevProps.dataUrl !== this.props.dataUrl) {
// noinspection JSIgnoredPromiseFromCall
this.loadData();
}
if (this.props.selection !== prevProps.selection || this.state.treeData != prevState.treeData) {
if (this.state.treeData != prevState.treeData) {
this.tree.reload(this.sanitizeTreeData(this.state.treeData));

View file

@ -38,7 +38,7 @@ export class UntrustedContentHost extends Component {
this.contentNodeIsLoaded = false;
this.state = {
hasAccessToken: false,
hasAccessToken: false
};
this.receiveMessageHandler = ::this.receiveMessage;
@ -175,7 +175,8 @@ export class UntrustedContentHost extends Component {
render() {
return (
<iframe className={styles.untrustedContent + ' ' + this.props.className} ref={node => this.contentNode = node} src={getSandboxUrl(this.props.contentSrc)} onLoad={::this.contentNodeLoaded}> </iframe>
// The 40 px below corresponds to the height in .sandbox-loading-message
<iframe className={styles.untrustedContent + ' ' + this.props.className} height="40px" ref={node => this.contentNode = node} src={getSandboxUrl(this.props.contentSrc)} onLoad={::this.contentNodeLoaded}></iframe>
);
}
}
@ -218,10 +219,10 @@ export class UntrustedContentRoot extends Component {
async receiveMessage(evt) {
const msg = evt.data;
if (msg.type === 'initAvailable' && !this.state.initialized) {
if (msg.type === 'initAvailable') {
this.sendMessage('initNeeded');
} else if (msg.type === 'init' && !this.state.initialized) {
} else if (msg.type === 'init') {
setRestrictedAccessToken(msg.data.accessToken);
this.setState({
initialized: true,
@ -255,7 +256,7 @@ export class UntrustedContentRoot extends Component {
return this.props.render(this.state.contentProps);
} else {
return (
<div>
<div className="sandbox-loading-message">
{t('loading-1')}
</div>
);

View file

@ -82,6 +82,8 @@ export default class Login extends Component {
/* This ensures we get config for the authenticated user */
window.location = nextUrl;
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('pleaseEnterYourCredentialsAndTryAgain'));
}
} catch (error) {

View file

@ -1,4 +1,10 @@
@import url('https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,400i,700,700i|Ubuntu:300,300i,400,400i,700,700i&subset=latin-ext');
$font-family-sans-serif: 'Ubuntu', sans-serif;
$font-family-monospace: 'Ubuntu Mono', monospace;
$fa-font-path: "../static-npm/fontawesome";
$enable-print-styles: false;
@import "./variables.scss";
@import "node_modules/@coreui/coreui/scss/coreui.scss";
@ -13,6 +19,19 @@ $fa-font-path: "../static-npm/fontawesome";
body.mailtrain {
background-color: white;
&.sandbox {
overflow-x: hidden;
}
&.inside-iframe {
overflow: hidden;
}
.sandbox-loading-message {
// The 40 px below corresponds to the height in in UntrustedContentHost.render
height: 40px;
}
.dropdown-item {
border-bottom: none 0px;
}

View file

@ -52,6 +52,7 @@ import {withComponentMixins} from "../lib/decorator-helpers";
])
export default class CUD extends Component {
constructor(props) {
console.log('constructor')
super(props);
this.templateTypes = getTemplateTypes(props.t);
@ -74,7 +75,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
wizard: PropTypes.string,
entity: PropTypes.object
entity: PropTypes.object,
setPanelInFullScreen: PropTypes.func
}
onTypeChanged(mutStateData, key, oldType, type) {
@ -209,6 +211,7 @@ export default class CUD extends Component {
}
async setElementInFullscreen(elementInFullscreen) {
this.props.setPanelInFullScreen(elementInFullscreen);
this.setState({
elementInFullscreen
});

View file

@ -28,7 +28,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/templates/${params.templateId}/edit`,
visible: resolved => resolved.template.permissions.includes('edit'),
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} setPanelInFullScreen={props.setPanelInFullScreen} />
},
files: {
title: t('files'),