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:
parent
4f408a26d5
commit
e0bee9ed42
28 changed files with 353 additions and 97 deletions
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
|
|
7
client/src/lib/bootstrap-components.js
vendored
7
client/src/lib/bootstrap-components.js
vendored
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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">© 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">© 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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue