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
				
			
		
							
								
								
									
										2
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
[submodule "mvis/ivis-core"]
 | 
			
		||||
	path = mvis/ivis-core
 | 
			
		||||
	url = git@gitlab.d3s.mff.cuni.cz:evif/ivis-core.git
 | 
			
		||||
	url = https://gitlab.d3s.mff.cuni.cz/evif/ivis-core.git
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
Subproject commit a9ad4bab17475ab8646a0294338df59aa3864cb9
 | 
			
		||||
Subproject commit 7d15f154c933c4789d6c9b288fbdf7437be3d856
 | 
			
		||||
							
								
								
									
										55
									
								
								server/lib/activity-log.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								server/lib/activity-log.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
async function _logActivity(typeId, data) {
 | 
			
		||||
    // TODO
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Extra data:
 | 
			
		||||
 | 
			
		||||
campaign:
 | 
			
		||||
- status : CampaignStatus
 | 
			
		||||
 | 
			
		||||
list:
 | 
			
		||||
- subscriptionId
 | 
			
		||||
- subscriptionStatus : SubscriptionStatus
 | 
			
		||||
- fieldId
 | 
			
		||||
- segmentId
 | 
			
		||||
- importId
 | 
			
		||||
- importStatus : ImportStatus
 | 
			
		||||
*/
 | 
			
		||||
async function logEntityActivity(entityTypeId, activityType, entityId, extraData = {}) {
 | 
			
		||||
    const data = {
 | 
			
		||||
        ...extraData,
 | 
			
		||||
        type: activityType,
 | 
			
		||||
        entity: entityId
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    await _logActivity(entityTypeId, data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function logCampaignTrackerActivity(activityType, campaignId, listId, subscriptionId, extraData = {}) {
 | 
			
		||||
    const data = {
 | 
			
		||||
        ...extraData,
 | 
			
		||||
        type: activityType,
 | 
			
		||||
        campaign: campaignId,
 | 
			
		||||
        list: listId,
 | 
			
		||||
        subscription: subscriptionId
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    await _logActivity('campaign_tracker', data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function logBlacklistActivity(activityType, email) {
 | 
			
		||||
    const data = {
 | 
			
		||||
        ...extraData,
 | 
			
		||||
        type: activityType,
 | 
			
		||||
        email
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    await _logActivity('blacklist', data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports.logEntityActivity = logEntityActivity;
 | 
			
		||||
module.exports.logBlacklistActivity = logBlacklistActivity;
 | 
			
		||||
module.exports.logCampaignTrackerActivity = logCampaignTrackerActivity;
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,8 @@ const fork = require('child_process').fork;
 | 
			
		|||
const log = require('./log');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const {ImportStatus, RunStatus} = require('../../shared/imports');
 | 
			
		||||
const {ListActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('./activity-log');
 | 
			
		||||
 | 
			
		||||
let messageTid = 0;
 | 
			
		||||
let importerProcess;
 | 
			
		||||
| 
						 | 
				
			
			@ -18,11 +20,17 @@ function spawn(callback) {
 | 
			
		|||
    log.verbose('Importer', 'Spawning importer process');
 | 
			
		||||
 | 
			
		||||
    knex.transaction(async tx => {
 | 
			
		||||
        await tx('imports').where('status', ImportStatus.PREP_RUNNING).update({status: ImportStatus.PREP_SCHEDULED});
 | 
			
		||||
        await tx('imports').where('status', ImportStatus.PREP_STOPPING).update({status: ImportStatus.PREP_FAILED});
 | 
			
		||||
        const updateStatus = async (fromStatus, toStatus) => {
 | 
			
		||||
            for (const impt of await tx('imports').where('status', fromStatus).select(['id', 'list'])) {
 | 
			
		||||
                await tx('imports').where('id', impt.id).update({status: toStatus});
 | 
			
		||||
                await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: toStatus});
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await tx('imports').where('status', ImportStatus.RUN_RUNNING).update({status: ImportStatus.RUN_SCHEDULED});
 | 
			
		||||
        await tx('imports').where('status', ImportStatus.RUN_STOPPING).update({status: ImportStatus.RUN_FAILED});
 | 
			
		||||
        await updateStatus(ImportStatus.PREP_RUNNING, ImportStatus.PREP_SCHEDULED);
 | 
			
		||||
        await updateStatus(ImportStatus.PREP_STOPPING, ImportStatus.PREP_FAILED);
 | 
			
		||||
        await updateStatus(ImportStatus.RUN_RUNNING, ImportStatus.RUN_SCHEDULED);
 | 
			
		||||
        await updateStatus(ImportStatus.RUN_STOPPING, ImportStatus.RUN_FAILED);
 | 
			
		||||
 | 
			
		||||
        await tx('import_runs').where('status', RunStatus.RUNNING).update({status: RunStatus.SCHEDULED});
 | 
			
		||||
        await tx('import_runs').where('status', RunStatus.STOPPING).update({status: RunStatus.FAILED});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,10 @@ const shares = require('./shares');
 | 
			
		|||
const tools = require('../lib/tools');
 | 
			
		||||
const { enforce } = require('../lib/helpers');
 | 
			
		||||
 | 
			
		||||
const {BlacklistActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async function listDTAjax(context, params) {
 | 
			
		||||
    shares.enforceGlobalPermission(context, 'manageBlacklist');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,14 +48,21 @@ async function add(context, email) {
 | 
			
		|||
        if (!existing) {
 | 
			
		||||
            await tx('blacklist').insert({email});
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await activityLog.logBlacklistActivity(BlacklistActivityType.ADD, email);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function remove(context, email) {
 | 
			
		||||
    enforce(email, 'Email has to be set');
 | 
			
		||||
 | 
			
		||||
    shares.enforceGlobalPermission(context, 'manageBlacklist');
 | 
			
		||||
    await knex('blacklist').where('email', email).del();
 | 
			
		||||
    return await knex.transaction(async tx => {
 | 
			
		||||
        shares.enforceGlobalPermission(context, 'manageBlacklist');
 | 
			
		||||
 | 
			
		||||
        await tx('blacklist').where('email', email).del();
 | 
			
		||||
 | 
			
		||||
        await activityLog.logBlacklistActivity(BlacklistActivityType.REMOVE, email);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function isBlacklisted(email) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,9 @@ const {LinkId} = require('./links');
 | 
			
		|||
const feedcheck = require('../lib/feedcheck');
 | 
			
		||||
const contextHelpers = require('../lib/context-helpers');
 | 
			
		||||
 | 
			
		||||
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
 | 
			
		||||
    'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -533,6 +536,8 @@ async function _createTx(tx, context, entity, content) {
 | 
			
		|||
                }).where('id', id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('campaign', EntityActivityType.CREATE, id, {status: filteredEntity.status});
 | 
			
		||||
 | 
			
		||||
        return id;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -591,6 +596,8 @@ async function updateWithConsistencyCheck(context, entity, content) {
 | 
			
		|||
        await tx('campaigns').where('id', entity.id).update(filteredEntity);
 | 
			
		||||
 | 
			
		||||
        await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('campaign', EntityActivityType.UPDATE, entity.id, {status: filteredEntity.status});
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -628,6 +635,8 @@ async function _removeTx(tx, context, id, existing = null) {
 | 
			
		|||
        .del();
 | 
			
		||||
 | 
			
		||||
    await tx('campaigns').where('id', id).del();
 | 
			
		||||
 | 
			
		||||
    await activityLog.logEntityActivity('campaign', EntityActivityType.REMOVE, id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -863,6 +872,8 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
 | 
			
		|||
            status: newState,
 | 
			
		||||
            scheduled
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: newState});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    senders.scheduleCheck();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,8 @@ const { cleanupFromPost } = require('../lib/helpers');
 | 
			
		|||
const Handlebars = require('handlebars');
 | 
			
		||||
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
 | 
			
		||||
const { getMergeTagsForBases } = require('../../shared/templates');
 | 
			
		||||
const {ListActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
 | 
			
		||||
| 
						 | 
				
			
			@ -565,6 +567,8 @@ async function createTx(tx, context, listId, entity) {
 | 
			
		|||
        await knex.schema.raw('ALTER TABLE `subscription__' + listId + '` ADD `source_' + columnName +'` int(11) DEFAULT NULL');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await activityLog.logEntityActivity('list', ListActivityType.CREATE_FIELD, listId, {fieldId: id});
 | 
			
		||||
 | 
			
		||||
    return id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -594,6 +598,8 @@ async function updateWithConsistencyCheck(context, listId, entity) {
 | 
			
		|||
 | 
			
		||||
        await tx('custom_fields').where({list: listId, id: entity.id}).update(filterObject(entity, allowedKeysUpdate));
 | 
			
		||||
        await _sortIn(tx, listId, entity.id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.UPDATE_FIELD, listId, {fieldId: entity.id});
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -620,6 +626,8 @@ async function removeTx(tx, context, listId, id) {
 | 
			
		|||
 | 
			
		||||
        await segments.removeRulesByColumnTx(tx, context, listId, existing.column);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await activityLog.logEntityActivity('list', ListActivityType.REMOVE_FIELD, listId, {fieldId: id});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function remove(context, listId, id) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,8 @@ const {ImportSource, MappingType, ImportStatus, RunStatus, prepFinished, prepFin
 | 
			
		|||
const fs = require('fs-extra-promise');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const importer = require('../lib/importer');
 | 
			
		||||
const {ListActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
const files = require('./files');
 | 
			
		||||
const filesDir = path.join(files.filesDir, 'imports');
 | 
			
		||||
| 
						 | 
				
			
			@ -117,6 +119,8 @@ async function create(context, listId, entity, files) {
 | 
			
		|||
        const ids = await tx('imports').insert(filteredEntity);
 | 
			
		||||
        const id = ids[0];
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.CREATE_IMPORT, listId, {importId: id, importStatus: entity.status});
 | 
			
		||||
 | 
			
		||||
        return id;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -148,6 +152,8 @@ async function updateWithConsistencyCheck(context, listId, entity) {
 | 
			
		|||
        filteredEntity.mapping = JSON.stringify(filteredEntity.mapping);
 | 
			
		||||
 | 
			
		||||
        await tx('imports').where({list: listId, id: entity.id}).update(filteredEntity);
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.UPDATE_IMPORT, listId, {importId: entity.id, importStatus: entity.status});
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -170,6 +176,8 @@ async function removeTx(tx, context, listId, id) {
 | 
			
		|||
    await tx('import_failed').whereIn('run', function() {this.from('import_runs').select('id').where('import', id)}).del();
 | 
			
		||||
    await tx('import_runs').where('import', id).del();
 | 
			
		||||
    await tx('imports').where({list: listId, id}).del();
 | 
			
		||||
 | 
			
		||||
    await activityLog.logEntityActivity('list', ListActivityType.REMOVE_IMPORT, listId, {importId: id});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function remove(context, listId, id) {
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +216,8 @@ async function start(context, listId, id) {
 | 
			
		|||
            status: RunStatus.SCHEDULED,
 | 
			
		||||
            mapping: entity.mapping
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, listId, {importId: id, importStatus: ImportStatus.RUN_SCHEDULED});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    importer.scheduleCheck();
 | 
			
		||||
| 
						 | 
				
			
			@ -234,6 +244,8 @@ async function stop(context, listId, id) {
 | 
			
		|||
        await tx('import_runs').where('import', id).whereIn('status', [RunStatus.SCHEDULED, RunStatus.RUNNING]).update({
 | 
			
		||||
            status: RunStatus.STOPPING
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, listId, {importId: id, importStatus: ImportStatus.RUN_STOPPING});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    importer.scheduleCheck();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,9 @@ const imports = require('./imports');
 | 
			
		|||
const entitySettings = require('../lib/entity-settings');
 | 
			
		||||
const dependencyHelpers = require('../lib/dependency-helpers');
 | 
			
		||||
 | 
			
		||||
const {EntityActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
const {UnsubscriptionMode, FieldWizard} = require('../../shared/lists');
 | 
			
		||||
 | 
			
		||||
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace', 'to_name', 'listunsubscribe_disabled', 'send_configuration']);
 | 
			
		||||
| 
						 | 
				
			
			@ -196,6 +199,8 @@ async function create(context, entity) {
 | 
			
		|||
            await fields.createTx(tx, context, id, fld);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', EntityActivityType.CREATE, id);
 | 
			
		||||
 | 
			
		||||
        return id;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -221,6 +226,8 @@ async function updateWithConsistencyCheck(context, entity) {
 | 
			
		|||
        await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));
 | 
			
		||||
 | 
			
		||||
        await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: entity.id });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', EntityActivityType.UPDATE, entity.id);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -244,6 +251,8 @@ async function remove(context, id) {
 | 
			
		|||
 | 
			
		||||
        await tx('lists').where('id', id).del();
 | 
			
		||||
        await knex.schema.dropTableIfExists('subscription__' + id);
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', EntityActivityType.REMOVE, id);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,8 @@ const moment = require('moment');
 | 
			
		|||
const fields = require('./fields');
 | 
			
		||||
const subscriptions = require('./subscriptions');
 | 
			
		||||
const dependencyHelpers = require('../lib/dependency-helpers');
 | 
			
		||||
const {ListActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
const allowedKeys = new Set(['name', 'settings']);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -304,6 +306,8 @@ async function create(context, listId, entity) {
 | 
			
		|||
        const ids = await tx('segments').insert(filteredEntity);
 | 
			
		||||
        const id = ids[0];
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.CREATE_SEGMENT, listId, {segmentId: id});
 | 
			
		||||
 | 
			
		||||
        return id;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -327,6 +331,8 @@ async function updateWithConsistencyCheck(context, listId, entity) {
 | 
			
		|||
        await _validateAndPreprocess(tx, listId, entity, false);
 | 
			
		||||
 | 
			
		||||
        await tx('segments').where({list: listId, id: entity.id}).update(filterObject(entity, allowedKeys));
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.UPDATE_SEGMENT, listId, {segmentId: entity.id});
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -346,6 +352,8 @@ async function removeTx(tx, context, listId, id) {
 | 
			
		|||
 | 
			
		||||
    // The listId "where" is here to prevent deleting segment of a list for which a user does not have permission
 | 
			
		||||
    await tx('segments').where({list: listId, id}).del();
 | 
			
		||||
 | 
			
		||||
    await activityLog.logEntityActivity('list', ListActivityType.REMOVE_SEGMENT, listId, {segmentId: id});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function remove(context, listId, id) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,8 @@ const contextHelpers = require('../lib/context-helpers');
 | 
			
		|||
const tools = require('../lib/tools');
 | 
			
		||||
const shares = require('../models/shares');
 | 
			
		||||
const { tLog } = require('../lib/translate');
 | 
			
		||||
const {ListActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const csvparse = require('csv-parse');
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +43,8 @@ function prepareCsv(impt) {
 | 
			
		|||
            error: msg + '\n' + err.message
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_FAILED});
 | 
			
		||||
 | 
			
		||||
        await fsExtra.removeAsync(filePath);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +60,8 @@ function prepareCsv(impt) {
 | 
			
		|||
            error: null
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_FINISHED});
 | 
			
		||||
 | 
			
		||||
        await fsExtra.removeAsync(filePath);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -263,12 +269,16 @@ async function _execImportRun(impt, handlers) {
 | 
			
		|||
            status: ImportStatus.RUN_FINISHED
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.RUN_FINISHED});
 | 
			
		||||
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        await knex('imports').where('id', impt.id).update({
 | 
			
		||||
            last_run: new Date(),
 | 
			
		||||
            error: err.message,
 | 
			
		||||
            status: ImportStatus.RUN_FAILED
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_FAILED});
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -361,14 +371,20 @@ async function getTask() {
 | 
			
		|||
 | 
			
		||||
            if (impt.source === ImportSource.CSV_FILE && impt.status === ImportStatus.PREP_SCHEDULED) {
 | 
			
		||||
                await tx('imports').where('id', impt.id).update('status', ImportStatus.PREP_RUNNING);
 | 
			
		||||
                await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_RUNNING});
 | 
			
		||||
 | 
			
		||||
                return () => prepareCsv(impt);
 | 
			
		||||
 | 
			
		||||
            } else if (impt.status === ImportStatus.RUN_SCHEDULED && impt.mapping_type === MappingType.BASIC_SUBSCRIBE) {
 | 
			
		||||
                await tx('imports').where('id', impt.id).update('status', ImportStatus.RUN_RUNNING);
 | 
			
		||||
                await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.RUN_RUNNING});
 | 
			
		||||
 | 
			
		||||
                return () => basicSubscribe(impt);
 | 
			
		||||
 | 
			
		||||
            } else if (impt.status === ImportStatus.RUN_SCHEDULED && impt.mapping_type === MappingType.BASIC_UNSUBSCRIBE) {
 | 
			
		||||
                await tx('imports').where('id', impt.id).update('status', ImportStatus.RUN_RUNNING);
 | 
			
		||||
                await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.RUN_RUNNING});
 | 
			
		||||
 | 
			
		||||
                return () => basicUnsubscribe(impt);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,9 @@ const {CampaignStatus, CampaignType} = require('../../shared/campaigns');
 | 
			
		|||
const { enforce } = require('../lib/helpers');
 | 
			
		||||
const campaigns = require('../models/campaigns');
 | 
			
		||||
const builtinZoneMta = require('../lib/builtin-zone-mta');
 | 
			
		||||
const {CampaignActivityType} = require('../../shared/activity-log');
 | 
			
		||||
const activityLog = require('../lib/activity-log');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
let messageTid = 0;
 | 
			
		||||
const workerProcesses = new Map();
 | 
			
		||||
| 
						 | 
				
			
			@ -127,6 +130,8 @@ async function processCampaign(campaignId) {
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        await knex('campaigns').where('id', campaignId).update({status: CampaignStatus.FINISHED});
 | 
			
		||||
        await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: CampaignStatus.FINISHED});
 | 
			
		||||
 | 
			
		||||
        messageQueue.delete(campaignId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -214,6 +219,7 @@ async function scheduleCampaigns() {
 | 
			
		|||
 | 
			
		||||
                if (scheduledCampaign) {
 | 
			
		||||
                    await tx('campaigns').where('id', scheduledCampaign.id).update({status: CampaignStatus.SENDING});
 | 
			
		||||
                    await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, scheduledCampaign.id, {status: CampaignStatus.SENDING});
 | 
			
		||||
                    campaignId = scheduledCampaign.id;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										47
									
								
								shared/activity-log.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								shared/activity-log.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
const EntityActivityType = {
 | 
			
		||||
    CREATE: 1,
 | 
			
		||||
    UPDATE: 2,
 | 
			
		||||
    REMOVE: 3,
 | 
			
		||||
    MAX: 3
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CampaignActivityType = {
 | 
			
		||||
    STATUS_CHANGE: EntityActivityType.MAX + 1
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ListActivityType = {
 | 
			
		||||
    CREATE_SUBSCRIPTION: EntityActivityType.MAX + 1,
 | 
			
		||||
    UPDATE_SUBSCRIPTION: EntityActivityType.MAX + 2,
 | 
			
		||||
    REMOVE_SUBSCRIPTION: EntityActivityType.MAX + 3,
 | 
			
		||||
    SUBSCRIPTION_STATUS_CHANGE: EntityActivityType.MAX + 4,
 | 
			
		||||
    CREATE_FIELD: EntityActivityType.MAX + 5,
 | 
			
		||||
    UPDATE_FIELD: EntityActivityType.MAX + 6,
 | 
			
		||||
    REMOVE_FIELD: EntityActivityType.MAX + 7,
 | 
			
		||||
    CREATE_SEGMENT: EntityActivityType.MAX + 5,
 | 
			
		||||
    UPDATE_SEGMENT: EntityActivityType.MAX + 6,
 | 
			
		||||
    REMOVE_SEGMENT: EntityActivityType.MAX + 7,
 | 
			
		||||
    CREATE_IMPORT: EntityActivityType.MAX + 8,
 | 
			
		||||
    UPDATE_IMPORT: EntityActivityType.MAX + 9,
 | 
			
		||||
    REMOVE_IMPORT: EntityActivityType.MAX + 10,
 | 
			
		||||
    IMPORT_STATUS_CHANGE: EntityActivityType.MAX + 11,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CampaignTrackerActivityType = {
 | 
			
		||||
    DELIVERED: 1,
 | 
			
		||||
    BOUNCED: 2,
 | 
			
		||||
    OPENED: 3,
 | 
			
		||||
    CLICKED: 4
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const BlacklistActivityType = {
 | 
			
		||||
    ADD: 1,
 | 
			
		||||
    REMOVE: 2
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
module.exports.EntityActivityType = EntityActivityType;
 | 
			
		||||
module.exports.BlacklistActivityType = BlacklistActivityType;
 | 
			
		||||
module.exports.CampaignActivityType = CampaignActivityType;
 | 
			
		||||
module.exports.ListActivityType = ListActivityType;
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue