Some preparations for activity log.

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

2
.gitmodules vendored
View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -189,7 +189,9 @@ class TertiaryNavBar extends Component {
class RouteContent extends Component {
constructor(props) {
super(props);
this.state = {};
this.state = {
panelInFullScreen: props.route.panelInFullScreen
};
if (Object.keys(props.route.resolve).length === 0) {
this.state.resolved = {};
@ -200,6 +202,8 @@ class RouteContent extends Component {
this.forceUpdate();
}
};
this.setPanelInFullScreen = panelInFullScreen => this.setState({ panelInFullScreen });
}
static propTypes = {
@ -208,7 +212,9 @@ class RouteContent extends Component {
}
@withAsyncErrorHandler
async resolve(props) {
async resolve() {
const props = this.props;
if (Object.keys(props.route.resolve).length === 0) {
this.setState({
resolved: {}
@ -237,18 +243,16 @@ class RouteContent extends Component {
componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.resolve(this.props);
this.resolve();
this.registerSidebarAnimationListener();
}
componentDidUpdate() {
componentDidUpdate(prevProps) {
this.registerSidebarAnimationListener();
}
componentWillReceiveProps(nextProps) {
if (this.props.match.params !== nextProps.match.params && needsResolve(this.props.route, nextProps.route, this.props.match, nextProps.match)) {
if (this.props.match.params !== prevProps.match.params && needsResolve(prevProps.route, this.props.route, prevProps.match, this.props.match)) {
// noinspection JSIgnoredPromiseFromCall
this.resolve(nextProps);
this.resolve();
}
}
@ -264,6 +268,8 @@ class RouteContent extends Component {
const showSidebar = !!route.secondaryMenuComponent;
const panelInFullScreen = this.state.panelInFullScreen;
if (!route.panelRender && !route.panelComponent && route.link) {
let link;
if (typeof route.link === 'function') {
@ -283,7 +289,9 @@ class RouteContent extends Component {
const compProps = {
match: this.props.match,
location: this.props.location,
resolved
resolved,
setPanelInFullScreen: this.setPanelInFullScreen,
panelInFullScreen: this.state.panelInFullScreen
};
let panel;
@ -301,20 +309,27 @@ class RouteContent extends Component {
secondaryMenu = React.createElement(route.secondaryMenuComponent, compProps);
}
content = (
<>
<div className="mt-breadcrumb-and-tertiary-navbar">
<Breadcrumb route={route} params={params} resolved={resolved}/>
<TertiaryNavBar route={route} params={params} resolved={resolved}/>
</div>
<div className="container-fluid">
{this.props.flashMessage}
{panel}
</div>
</>
const panelContent = (
<div key="panel" className="container-fluid">
{this.props.flashMessage}
{panel}
</div>
);
if (panelInFullScreen) {
content = panelContent;
} else {
content = (
<>
<div key="tertiaryNav" className="mt-breadcrumb-and-tertiary-navbar">
<Breadcrumb route={route} params={params} resolved={resolved}/>
<TertiaryNavBar route={route} params={params} resolved={resolved}/>
</div>
{panelContent}
</>
);
}
} else {
content = (
<div className="container-fluid">
@ -323,45 +338,57 @@ class RouteContent extends Component {
);
}
return (
<div className={"app " + (showSidebar ? 'sidebar-lg-show' : '')}>
<header className="app-header">
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
{showSidebar &&
<button className="navbar-toggler sidebar-toggler" data-toggle="sidebar-show" type="button">
<span className="navbar-toggler-icon"/>
</button>
}
<Link className="navbar-brand" to="/"><div><Icon icon="envelope"/> Mailtrain</div></Link>
<button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#mtMainNavbar" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"/>
</button>
<div className="collapse navbar-collapse" id="mtMainNavbar">
{primaryMenu}
</div>
</nav>
</header>
<div className="app-body">
{showSidebar &&
<div className="sidebar">
{secondaryMenu}
if (panelInFullScreen) {
return (
<div key="app" className="app panel-in-fullscreen">
<div key="appBody" className="app-body">
<main key="main" className="main">
{content}
</main>
</div>
}
<main className="main">
{content}
</main>
</div>
);
<footer className="app-footer">
<div className="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGitHub')}</a></div>
</footer>
</div>
);
} else {
return (
<div key="app" className={"app " + (showSidebar ? 'sidebar-lg-show' : '')}>
<header key="appHeader" className="app-header">
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
{showSidebar &&
<button className="navbar-toggler sidebar-toggler" data-toggle="sidebar-show" type="button">
<span className="navbar-toggler-icon"/>
</button>
}
<Link className="navbar-brand" to="/"><div><Icon icon="envelope"/> Mailtrain</div></Link>
<button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#mtMainNavbar" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"/>
</button>
<div className="collapse navbar-collapse" id="mtMainNavbar">
{primaryMenu}
</div>
</nav>
</header>
<div key="appBody" className="app-body">
{showSidebar &&
<div key="sidebar" className="sidebar">
{secondaryMenu}
</div>
}
<main key="main" className="main">
{content}
</main>
</div>
<footer key="appFooter" className="app-footer">
<div className="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGitHub')}</a></div>
</footer>
</div>
);
}
}
}
}
@ -656,7 +683,7 @@ export function getLanguageChooser(t) {
const label = langDesc.getLabel(t);
languageOptions.push(
<DropdownActionLink key={lng} onClickAsync={() => i18n.changeLanguage(langDesc.longCode)}>{label}</DropdownActionLink>
<DropdownActionLink key={lng} onClickAsync={async () => i18n.changeLanguage(langDesc.longCode)}>{label}</DropdownActionLink>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit a9ad4bab17475ab8646a0294338df59aa3864cb9
Subproject commit 7d15f154c933c4789d6c9b288fbdf7437be3d856

View 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;

View file

@ -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});

View file

@ -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) {

View file

@ -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();

View file

@ -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) {

View file

@ -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();

View file

@ -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);
});
}

View file

@ -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) {

View file

@ -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);
}

View file

@ -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
View 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;