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,19 +309,26 @@ 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">
|
||||
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 = (
|
||||
|
@ -323,10 +338,21 @@ class RouteContent extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
if (panelInFullScreen) {
|
||||
return (
|
||||
<div className={"app " + (showSidebar ? 'sidebar-lg-show' : '')}>
|
||||
<header className="app-header">
|
||||
<div key="app" className="app panel-in-fullscreen">
|
||||
<div key="appBody" className="app-body">
|
||||
<main key="main" className="main">
|
||||
{content}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
} else {
|
||||
return (
|
||||
<div key="app" className={"app " + (showSidebar ? 'sidebar-lg-show' : '')}>
|
||||
<header key="appHeader" className="app-header">
|
||||
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
{showSidebar &&
|
||||
<button className="navbar-toggler sidebar-toggler" data-toggle="sidebar-show" type="button">
|
||||
|
@ -346,18 +372,18 @@ class RouteContent extends Component {
|
|||
</nav>
|
||||
</header>
|
||||
|
||||
<div className="app-body">
|
||||
<div key="appBody" className="app-body">
|
||||
{showSidebar &&
|
||||
<div className="sidebar">
|
||||
<div key="sidebar" className="sidebar">
|
||||
{secondaryMenu}
|
||||
</div>
|
||||
}
|
||||
<main className="main">
|
||||
<main key="main" className="main">
|
||||
{content}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer className="app-footer">
|
||||
<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>
|
||||
|
@ -365,6 +391,7 @@ class RouteContent extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@withRouter
|
||||
|
@ -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');
|
||||
|
||||
return await knex.transaction(async tx => {
|
||||
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||
await knex('blacklist').where('email', email).del();
|
||||
|
||||
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…
Reference in a new issue