From e0bee9ed426c1d489d95f9d9228a9d62b85d6ddc Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Thu, 7 Feb 2019 14:38:32 +0000 Subject: [PATCH 01/41] Some preparations for activity log. Fixed issue #524 Table now displays horizontal scrollbar when the viewport is too narrow (typically on mobile) --- .gitmodules | 2 +- client/src/campaigns/Content.js | 4 +- client/src/campaigns/root.js | 3 +- client/src/lib/bootstrap-components.js | 7 +- client/src/lib/i18n.js | 6 +- client/src/lib/modals.js | 2 +- client/src/lib/page-common.js | 2 + client/src/lib/page.js | 143 +++++++++++++++---------- client/src/lib/styles.scss | 5 + client/src/lib/table.js | 6 +- client/src/lib/tree.js | 26 +++-- client/src/lib/untrusted.js | 11 +- client/src/login/Login.js | 2 + client/src/scss/mailtrain.scss | 19 ++++ client/src/templates/CUD.js | 5 +- client/src/templates/root.js | 2 +- mvis/ivis-core | 2 +- server/lib/activity-log.js | 55 ++++++++++ server/lib/importer.js | 16 ++- server/models/blacklist.js | 15 ++- server/models/campaigns.js | 11 ++ server/models/fields.js | 8 ++ server/models/imports.js | 12 +++ server/models/lists.js | 9 ++ server/models/segments.js | 8 ++ server/services/importer.js | 16 +++ server/services/sender-master.js | 6 ++ shared/activity-log.js | 47 ++++++++ 28 files changed, 353 insertions(+), 97 deletions(-) create mode 100644 server/lib/activity-log.js create mode 100644 shared/activity-log.js diff --git a/.gitmodules b/.gitmodules index ea1b2ddf..01b4e6d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/client/src/campaigns/Content.js b/client/src/campaigns/Content.js index 5e59c286..377e8e64 100644 --- a/client/src/campaigns/Content.js +++ b/client/src/campaigns/Content.js @@ -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 }); diff --git a/client/src/campaigns/root.js b/client/src/campaigns/root.js index cb43170b..30737026 100644 --- a/client/src/campaigns/root.js +++ b/client/src/campaigns/root.js @@ -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 => + panelRender: props => }, files: { title: t('files'), diff --git a/client/src/lib/bootstrap-components.js b/client/src/lib/bootstrap-components.js index eb518969..79fa487b 100644 --- a/client/src/lib/bootstrap-components.js +++ b/client/src/lib/bootstrap-components.js @@ -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 ( - + ); } } @@ -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 = - } - -
Mailtrain
- - - -
- {primaryMenu} -
- - - -
- {showSidebar && -
- {secondaryMenu} + if (panelInFullScreen) { + return ( +
+
+
+ {content} +
- } -
- {content} -
+ ); - -
- ); + } else { + return ( +
+
+ +
+ +
+ {showSidebar && +
+ {secondaryMenu} +
+ } +
+ {content} +
+
+ + +
+ ); + } } } } @@ -656,7 +683,7 @@ export function getLanguageChooser(t) { const label = langDesc.getLabel(t); languageOptions.push( - i18n.changeLanguage(langDesc.longCode)}>{label} + i18n.changeLanguage(langDesc.longCode)}>{label} ) } diff --git a/client/src/lib/styles.scss b/client/src/lib/styles.scss index 10356f30..fbfb3543 100644 --- a/client/src/lib/styles.scss +++ b/client/src/lib/styles.scss @@ -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; } diff --git a/client/src/lib/table.js b/client/src/lib/table.js index 48d87075..f613c6a9 100644 --- a/client/src/lib/table.js +++ b/client/src/lib/table.js @@ -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; diff --git a/client/src/lib/tree.js b/client/src/lib/tree.js index d7b21dfc..0a73a8b6 100644 --- a/client/src/lib/tree.js +++ b/client/src/lib/tree.js @@ -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)); diff --git a/client/src/lib/untrusted.js b/client/src/lib/untrusted.js index 325c228d..10293a1d 100644 --- a/client/src/lib/untrusted.js +++ b/client/src/lib/untrusted.js @@ -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 ( - + // The 40 px below corresponds to the height in .sandbox-loading-message + ); } } @@ -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 ( -
+
{t('loading-1')}
); diff --git a/client/src/login/Login.js b/client/src/login/Login.js index 4834ac0d..c2544663 100644 --- a/client/src/login/Login.js +++ b/client/src/login/Login.js @@ -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) { diff --git a/client/src/scss/mailtrain.scss b/client/src/scss/mailtrain.scss index 8067cc9a..5199dc77 100644 --- a/client/src/scss/mailtrain.scss +++ b/client/src/scss/mailtrain.scss @@ -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; } diff --git a/client/src/templates/CUD.js b/client/src/templates/CUD.js index bca79361..2d423707 100644 --- a/client/src/templates/CUD.js +++ b/client/src/templates/CUD.js @@ -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 }); diff --git a/client/src/templates/root.js b/client/src/templates/root.js index 668ebc45..1e9fcd0d 100644 --- a/client/src/templates/root.js +++ b/client/src/templates/root.js @@ -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 => + panelRender: props => }, files: { title: t('files'), diff --git a/mvis/ivis-core b/mvis/ivis-core index a9ad4bab..7d15f154 160000 --- a/mvis/ivis-core +++ b/mvis/ivis-core @@ -1 +1 @@ -Subproject commit a9ad4bab17475ab8646a0294338df59aa3864cb9 +Subproject commit 7d15f154c933c4789d6c9b288fbdf7437be3d856 diff --git a/server/lib/activity-log.js b/server/lib/activity-log.js new file mode 100644 index 00000000..cb0ce651 --- /dev/null +++ b/server/lib/activity-log.js @@ -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; \ No newline at end of file diff --git a/server/lib/importer.js b/server/lib/importer.js index b849b859..5e59dd64 100644 --- a/server/lib/importer.js +++ b/server/lib/importer.js @@ -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}); diff --git a/server/models/blacklist.js b/server/models/blacklist.js index 77669aac..95f92460 100644 --- a/server/models/blacklist.js +++ b/server/models/blacklist.js @@ -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) { diff --git a/server/models/campaigns.js b/server/models/campaigns.js index 97a7dc66..1736d557 100644 --- a/server/models/campaigns.js +++ b/server/models/campaigns.js @@ -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(); diff --git a/server/models/fields.js b/server/models/fields.js index 9b461cd6..d7bc7943 100644 --- a/server/models/fields.js +++ b/server/models/fields.js @@ -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) { diff --git a/server/models/imports.js b/server/models/imports.js index 495cb232..ffc8992c 100644 --- a/server/models/imports.js +++ b/server/models/imports.js @@ -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(); diff --git a/server/models/lists.js b/server/models/lists.js index b91a7d84..32ca4f00 100644 --- a/server/models/lists.js +++ b/server/models/lists.js @@ -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); }); } diff --git a/server/models/segments.js b/server/models/segments.js index 676cf88c..6868fed2 100644 --- a/server/models/segments.js +++ b/server/models/segments.js @@ -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) { diff --git a/server/services/importer.js b/server/services/importer.js index 292e01e3..31992955 100644 --- a/server/services/importer.js +++ b/server/services/importer.js @@ -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); } diff --git a/server/services/sender-master.js b/server/services/sender-master.js index b3d2f89d..4cd03a2c 100644 --- a/server/services/sender-master.js +++ b/server/services/sender-master.js @@ -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; } }); diff --git a/shared/activity-log.js b/shared/activity-log.js new file mode 100644 index 00000000..ea029f97 --- /dev/null +++ b/shared/activity-log.js @@ -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; \ No newline at end of file From d1a139882849ff0be2e7fcc4417fcc5c814facc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor?= <27339341+priethor@users.noreply.github.com> Date: Sun, 17 Feb 2019 00:49:12 +0100 Subject: [PATCH 02/41] Support for string based segment rules.... ...such as email fields. --- server/setup/knex/migrations/20170506102634_v1_to_v2.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/setup/knex/migrations/20170506102634_v1_to_v2.js b/server/setup/knex/migrations/20170506102634_v1_to_v2.js index cb73e843..4a1caa20 100644 --- a/server/setup/knex/migrations/20170506102634_v1_to_v2.js +++ b/server/setup/knex/migrations/20170506102634_v1_to_v2.js @@ -526,6 +526,7 @@ async function migrateSegments(knex) { switch (fieldType) { case 'text': + case 'string': case 'website': rules.push({ column: oldRule.column, value: oldSettings.value }); break; From 46ad0c7b4fc8ae516eac529214a318536f5f97b8 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 12:46:02 +0000 Subject: [PATCH 03/41] Fix for #531 --- server/lib/activity-log.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/lib/activity-log.js b/server/lib/activity-log.js index cb0ce651..de637599 100644 --- a/server/lib/activity-log.js +++ b/server/lib/activity-log.js @@ -42,7 +42,6 @@ async function logCampaignTrackerActivity(activityType, campaignId, listId, subs async function logBlacklistActivity(activityType, email) { const data = { - ...extraData, type: activityType, email }; From abed0d4af421097e8a667d4b8539c866a86c5330 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 12:46:32 +0000 Subject: [PATCH 04/41] Fixes in messages --- locales/en-US/common.json | 20 ++++++++++---------- mvis/ivis-core | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/en-US/common.json b/locales/en-US/common.json index aabcd974..cbadc703 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -10,11 +10,11 @@ "thisApiCallEitherInsertsANewSubscription": "This API call either inserts a new subscription or updates existing. Fields not included are left as is, so if you update only LAST_NAME value, then FIRST_NAME is kept untouched for an existing subscription.", "arguments": "arguments", "yourPersonalAccessToken": "your personal access token", - "subscribersEmailAddress": "subscriber\\'s email address", + "subscribersEmailAddress": "subscriber's email address", "required": "required", - "subscribersFirstName": "subscriber\\'s first name", - "subscribersLastName": "subscriber\\'s last name", - "subscribersTimezoneEgEuropeTallinnPstOr": "subscriber\\'s timezone (eg. \"Europe/Tallinn\", \"PST\" or \"UTC\"). If not set defaults to \"UTC\"", + "subscribersFirstName": "subscriber's first name", + "subscribersLastName": "subscriber's last name", + "subscribersTimezoneEgEuropeTallinnPstOr": "subscriber's timezone (eg. \"Europe/Tallinn\", \"PST\" or \"UTC\"). If not set defaults to \"UTC\"", "customFieldValueUseYesnoForOptionGroup": "custom field value. Use yes/no for option group values (checkboxes, radios, drop downs)", "additionalPostArguments": "Additional POST arguments", "setToYesIfYouWantToMakeSureTheEmailIs": "set to \"yes\" if you want to make sure the email is marked as subscribed even if it was previously marked as unsubscribed. If the email was already unsubscribed/blocked then subscription status is not changed", @@ -28,7 +28,7 @@ "thisApiCallCreatesANewCustomFieldForA": "This API call creates a new custom field for a list.", "fieldName": "field name", "oneOfTheFollowingTypes": "one of the following types:", - "ifTheTypeIsOptionThenYouAlsoNeedTo": "If the type is \\'option\\' then you also need to specify the parent element ID", + "ifTheTypeIsOptionThenYouAlsoNeedTo": "If the type is 'option' then you also need to specify the parent element ID", "templateForTheGroupElementIfNotSetThen": "Template for the group element. If not set, then values of the elements are joined with commas", "ifNotVisibleThenTheSubscriberCanNotView": "if not visible then the subscriber can not view or modify this value at the profile page", "getListOfBlacklistedEmails": "Get list of blacklisted emails", @@ -485,7 +485,7 @@ "formsPreview": "Forms Preview", "listToPreviewOn": "List To Preview On", "selectListWhoseFieldsWillBeUsedToPreview": "Select list whose fields will be used to preview the forms.", - "noteTheseLinksAreSolelyForAQuickPreview": "Note: These links are solely for a quick preview. To get the address of the subscription form, go to the list\\'s subscribers and click on \"Subscription Form\".", + "noteTheseLinksAreSolelyForAQuickPreview": "Note: These links are solely for a quick preview. To get the address of the subscription form, go to the list's subscribers and click on \"Subscription Form\".", "formPreview": "Form preview:", "templates": "Templates", "customFormsUseMjmlForFormattingSeeThe": "Custom forms use MJML for formatting. See the MJML documentation <1>here", @@ -768,7 +768,7 @@ "theVerpServerHostnameEgBouncesexamplecom": "The VERP server hostname, eg. bounces.example.com", "verpBounceHandlingServerHostnameThis": "VERP bounce handling server hostname. This hostname is used in the SMTP envelope FROM address and the MX DNS records should point to this server", "disableSenderHeader": "Disable sender header", - "withDmarcTheReturnPathAndFromAddressMust": "With DMARC, the Return-Path and From address must match the same domain. By default we get around this by using the VERP address in the Sender header, with the side effect that some email clients diplay an ugly on behalf of message. You can safely disable this Sender header if you\\'re not using DMARC or your VERP hostname is in the same domain as the From address.", + "withDmarcTheReturnPathAndFromAddressMust": "With DMARC, the Return-Path and From address must match the same domain. By default we get around this by using the VERP address in the Sender header, with the side effect that some email clients diplay an ugly on behalf of message. You can safely disable this Sender header if you're not using DMARC or your VERP hostname is in the same domain as the From address.", "mailtrainIsAbleToUseVerpBasedRoutingTo": "<0>Mailtrain is able to use VERP based routing to detect bounces. In this case the message is sent to the recipient using a custom VERP address as the return path of the message. If the message is not accepted a bounce email is sent to this special VERP address and thus a bounce is detected.", "toGetVerpWorkingYouNeedToSetUpADnsMx": "<0>To get VERP working you need to set up a DNS MX record that points to your Mailtrain hostname. You must also ensure that Mailtrain VERP interface is available from port 25 of your server (port 25 usually requires root user privileges). This way if anyone tries to send email to someuser@verp-hostname then the email should end up to this server.", "verpUsuallyOnlyWorksIfYouAreUsingYourOwn": "<0>VERP usually only works if you are using your own SMTP server. Regular relay services (SES, SparkPost, Gmail etc.) tend to remove the VERP address from the message.", @@ -786,8 +786,8 @@ "uswest2": "US-WEST-2", "euwest1": "EU-WEST-1", "builtinZoneMta": "Built-in ZoneMTA", - "dynamicConfigurationOfDkimKeysViaZoneMt": "Dynamic configuration of DKIM keys via ZoneMTA\\'s Mailtrain plugin (use this option for builtin ZoneMTA)", - "dynamicConfigurationOfDkimKeysViaZoneMt-1": "Dynamic configuration of DKIM keys via ZoneMTA\\'s HTTP config plugin", + "dynamicConfigurationOfDkimKeysViaZoneMt": "Dynamic configuration of DKIM keys via ZoneMTA's Mailtrain plugin", + "dynamicConfigurationOfDkimKeysViaZoneMt-1": "Dynamic configuration of DKIM keys via ZoneMTA's HTTP config plugin", "noDynamicConfigurationOfDkimKeys": "No dynamic configuration of DKIM keys", "mailerSettings": "Mailer Settings", "mailerType": "Mailer type", @@ -845,7 +845,7 @@ "passphraseForTheKeyIfSet": "Passphrase for the key if set", "onlyFillThisIfYourPrivateKeyIsEncrypted": "Only fill this if your private key is encrypted with a passphrase", "gpgPrivateKey": "GPG private key", - "beginsWithBeginPgpPrivateKeyBlock": "Begins with \\'-----BEGIN PGP PRIVATE KEY BLOCK-----\\'", + "beginsWithBeginPgpPrivateKeyBlock": "Begins with '-----BEGIN PGP PRIVATE KEY BLOCK-----'", "thisValueIsOptionalIfYouDoNotProvideA": "This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.", "onlyMessagesThatAreEncryptedCanBeSigned": "<0>Only messages that are encrypted can be signed. Subsribers who have not set up a GPG public key in their profile receive normal email messages. Users with GPG key set receive encrypted messages and if you have signing key also set, the messages are signed with this key.", "doNotUseSensitiveKeysHereThePrivateKey": "<0>Do not use sensitive keys here. The private key and passphrase are not encrypted in the database.", diff --git a/mvis/ivis-core b/mvis/ivis-core index 7d15f154..d260de2e 160000 --- a/mvis/ivis-core +++ b/mvis/ivis-core @@ -1 +1 @@ -Subproject commit 7d15f154c933c4789d6c9b288fbdf7437be3d856 +Subproject commit d260de2e153f9c1d6bc7af69c1c3d733073bab74 From 02b42a4982f2de3f942b988bc55813b748891eee Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 13:56:15 +0000 Subject: [PATCH 05/41] Updated instructions to use docker image on docker hub. Addresses #521 --- README.md | 10 +++++----- docker-compose-local.yml | 41 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 docker-compose-local.yml diff --git a/README.md b/README.md index 0cc70fac..878336c1 100644 --- a/README.md +++ b/README.md @@ -180,14 +180,12 @@ To deploy Mailtrain with Docker, you need the following three dependencies insta These are the steps to start Mailtrain via docker-compose: -1. Download Mailtrain using git +1. Download Mailtrain's docker-compose build file ``` - git clone https://github.com/Mailtrain-org/mailtrain.git - cd mailtrain - git checkout development + curl -O https://raw.githubusercontent.com/Mailtrain-org/mailtrain/master/docker-compose.yml ``` -2. Deploy Mailtrain via docker-compose (in the root directory of the Mailtrain project). This will take quite some time when run for the first time. Subsequent executions will be fast. +2. Deploy Mailtrain via docker-compose (in the directory to which you downloaded the `docker-compose.yml` file). This will take quite some time when run for the first time. Subsequent executions will be fast. ``` docker-compose up ``` @@ -201,6 +199,8 @@ These are the steps to start Mailtrain via docker-compose: 4. Authenticate as `admin`:`test` +The instructions above use an automatically built Docker image on DockerHub (https://hub.docker.com/r/mailtrain/mailtrain). If you want to build the Docker image yourself (e.g. when doing development), use the `docker-compose-local-build.yml` located in the project's root directory. + ## License diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 00000000..ce2ba6e8 --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,41 @@ +version: '3' + +services: + mysql: + image: mariadb:10.4 + environment: + - MYSQL_ROOT_PASSWORD=mailtrain + - MYSQL_DATABASE=mailtrain + - MYSQL_USER=mailtrain + - MYSQL_PASSWORD=mailtrain + volumes: + - mysql-data:/var/lib/mysql + + redis: + image: redis:5 + volumes: + - redis-data:/data + + mongo: + image: mongo:4-xenial + volumes: + - mongo-data:/data/db + + mailtrain: + build: . + command: ${MAILTRAIN_SETTINGS} + ports: + - "3000:3000" + - "3003:3003" + - "3004:3004" + volumes: + - mailtrain-files:/app/server/files + - mailtrain-reports:/app/protected/reports + +volumes: + mysql-data: + redis-data: + mongo-data: + mailtrain-files: + mailtrain-reports: + diff --git a/docker-compose.yml b/docker-compose.yml index ce2ba6e8..61055326 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: - mongo-data:/data/db mailtrain: - build: . + image: mailtrain/mailtrain:latest command: ${MAILTRAIN_SETTINGS} ports: - "3000:3000" From 8a843bcea4f5e108a47eaa713d8e1fc8259b0589 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 14:50:00 +0000 Subject: [PATCH 06/41] Fix in docker instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 878336c1..6f60f25f 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ These are the steps to start Mailtrain via docker-compose: 1. Download Mailtrain's docker-compose build file ``` - curl -O https://raw.githubusercontent.com/Mailtrain-org/mailtrain/master/docker-compose.yml + curl -O https://raw.githubusercontent.com/Mailtrain-org/mailtrain/development/docker-compose.yml ``` 2. Deploy Mailtrain via docker-compose (in the directory to which you downloaded the `docker-compose.yml` file). This will take quite some time when run for the first time. Subsequent executions will be fast. From 7f2a9ca9400f1ce6c2381e6172d6da5761aca6c8 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 15:00:00 +0000 Subject: [PATCH 07/41] Link to the v1 removed from v2 readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f60f25f..9b2a7c37 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mailtrain v2 (beta) -[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v10+) and MySQL (v8+) or MariaDB (v10+). +Mailtrain is a self hosted newsletter application built on Node.js (v10+) and MySQL (v8+) or MariaDB (v10+). ![](https://mailtrain.org/mailtrain.png) From df2a8c1cde429049fa86b3d94c7adc5745ee8745 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 17:18:44 +0000 Subject: [PATCH 08/41] Fix for #536 --- client/src/lib/tree.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/lib/tree.js b/client/src/lib/tree.js index 0a73a8b6..99e2b738 100644 --- a/client/src/lib/tree.js +++ b/client/src/lib/tree.js @@ -96,7 +96,8 @@ class TreeTable extends Component { } shouldComponentUpdate(nextProps, nextState) { - return this.props.selection !== nextProps.selection || this.state.treeData != nextState.treeData || this.props.className !== nextProps.className; + return this.props.selection !== nextProps.selection || this.props.data !== nextProps.data || this.props.dataUrl !== nextProps.dataUrl || + this.state.treeData != nextState.treeData || this.props.className !== nextProps.className; } // XSS protection From f7b5aef0e3e1e937d09f50760f1fadc196ce7510 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 17:18:59 +0000 Subject: [PATCH 09/41] Some more fixes Warning dialog about missing Javascript removed from subscription dialog. The warning would flash in any case (even when Javascript is activated) --- README.md | 1 - client/src/lib/styles.scss | 2 +- server/lib/mailers.js | 2 +- server/routes/subscription.js | 4 ---- .../subscription/partials/subscription-flash-messages.hbs | 7 ------- 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9b2a7c37..f338782b 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,6 @@ To deploy Mailtrain with Docker, you need the following three dependencies insta - [Docker](https://www.docker.com/) - [Docker Compose](https://docs.docker.com/compose/) -- Git - Typically already present. If not, just use the package manager of your OS distribution to install it. These are the steps to start Mailtrain via docker-compose: diff --git a/client/src/lib/styles.scss b/client/src/lib/styles.scss index fbfb3543..6a9db9ff 100644 --- a/client/src/lib/styles.scss +++ b/client/src/lib/styles.scss @@ -78,7 +78,7 @@ } .tableSelectTable.tableSelectTableHidden { - visibility: hidden; + display: none; height: 0px; margin-top: -15px; } diff --git a/server/lib/mailers.js b/server/lib/mailers.js index fb8768fe..ac81a040 100644 --- a/server/lib/mailers.js +++ b/server/lib/mailers.js @@ -173,7 +173,7 @@ async function _createTransport(sendConfiguration) { } }; - if (mailerType === MailerType.ZONE_MTA || mailerSettings.zoneMTAType === ZoneMTAType.BUILTIN) { + if (mailerType === MailerType.ZONE_MTA && mailerSettings.zoneMTAType === ZoneMTAType.BUILTIN) { transportOptions.host = config.builtinZoneMTA.host; transportOptions.port = config.builtinZoneMTA.port; transportOptions.secure = false; diff --git a/server/routes/subscription.js b/server/routes/subscription.js index 7dd846b4..b37dea8b 100644 --- a/server/routes/subscription.js +++ b/server/routes/subscription.js @@ -183,7 +183,6 @@ async function _renderSubscribe(req, res, list, subscription) { const htmlRenderer = await tools.getTemplate(data.template, req.locale); data.isWeb = true; - data.needsJsWarning = true; data.flashMessages = await captureFlashMessages(res); @@ -385,7 +384,6 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) const htmlRenderer = await tools.getTemplate(data.template, req.locale); data.isWeb = true; - data.needsJsWarning = true; data.isManagePreferences = true; data.flashMessages = await captureFlashMessages(res); @@ -435,7 +433,6 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r const htmlRenderer = await tools.getTemplate(data.template, req.locale); data.isWeb = true; - data.needsJsWarning = true; data.isManagePreferences = true; data.flashMessages = await captureFlashMessages(res); @@ -535,7 +532,6 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, const htmlRenderer = await tools.getTemplate(data.template, req.locale); data.isWeb = true; - data.needsJsWarning = true; data.flashMessages = await captureFlashMessages(res); res.send(htmlRenderer(data)); diff --git a/server/views/subscription/partials/subscription-flash-messages.hbs b/server/views/subscription/partials/subscription-flash-messages.hbs index d014cfc9..3ea515f3 100644 --- a/server/views/subscription/partials/subscription-flash-messages.hbs +++ b/server/views/subscription/partials/subscription-flash-messages.hbs @@ -1,8 +1 @@ {{{flashMessages}}} - -{{#if needsJsWarning}} - -{{/if}} From 433bf31bfaacc52e9ae72b515d96d55c76ad9a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor?= <27339341+priethor@users.noreply.github.com> Date: Sun, 17 Feb 2019 18:45:35 +0100 Subject: [PATCH 10/41] Adds 'type' property to migration of text-based segment rules In v2, text-based segment rules need a "type" property. As in v1 the value could contain % wildcards, the default type for migrated rules should be 'like' to support them. --- server/setup/knex/migrations/20170506102634_v1_to_v2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/setup/knex/migrations/20170506102634_v1_to_v2.js b/server/setup/knex/migrations/20170506102634_v1_to_v2.js index 4a1caa20..05369236 100644 --- a/server/setup/knex/migrations/20170506102634_v1_to_v2.js +++ b/server/setup/knex/migrations/20170506102634_v1_to_v2.js @@ -528,7 +528,7 @@ async function migrateSegments(knex) { case 'text': case 'string': case 'website': - rules.push({ column: oldRule.column, value: oldSettings.value }); + rules.push({ type: 'like', column: oldRule.column, value: oldSettings.value }); break; case 'number': if (oldSettings.range) { From f8ef57f1640a02fb8948d345963a0b0e10e29556 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 17 Feb 2019 17:47:27 +0000 Subject: [PATCH 11/41] Fixed bug that prevented sending via builtin zone mta. This but was introduced today. --- server/lib/mailers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/mailers.js b/server/lib/mailers.js index ac81a040..7880ce1a 100644 --- a/server/lib/mailers.js +++ b/server/lib/mailers.js @@ -173,7 +173,7 @@ async function _createTransport(sendConfiguration) { } }; - if (mailerType === MailerType.ZONE_MTA && mailerSettings.zoneMTAType === ZoneMTAType.BUILTIN) { + if (mailerType === MailerType.ZONE_MTA && mailerSettings.zoneMtaType === ZoneMTAType.BUILTIN) { transportOptions.host = config.builtinZoneMTA.host; transportOptions.port = config.builtinZoneMTA.port; transportOptions.secure = false; From 8d95f43dbc305b5a172a746ad16276668842a666 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Mon, 18 Feb 2019 20:36:44 +0000 Subject: [PATCH 12/41] Added feature to create template from another template. --- client/src/templates/CUD.js | 48 ++++++++++++++++++++++++++------- client/src/templates/helpers.js | 4 +-- server/lib/campaign-content.js | 32 ++++++++++++++++++++++ server/models/campaigns.js | 30 +-------------------- server/models/templates.js | 17 ++++++++++++ 5 files changed, 90 insertions(+), 41 deletions(-) create mode 100644 server/lib/campaign-content.js diff --git a/client/src/templates/CUD.js b/client/src/templates/CUD.js index 2d423707..f8234d7a 100644 --- a/client/src/templates/CUD.js +++ b/client/src/templates/CUD.js @@ -12,12 +12,12 @@ import { } from '../lib/page' import { Button, - ButtonRow, + ButtonRow, CheckBox, Dropdown, Form, FormSendMethod, InputField, - StaticField, + StaticField, TableSelect, TextArea, withForm } from '../lib/form'; @@ -41,6 +41,8 @@ import styles import {getUrl} from "../lib/urls"; import {TestSendModalDialog} from "./TestSendModalDialog"; import {withComponentMixins} from "../lib/decorator-helpers"; +import moment + from 'moment'; @withComponentMixins([ @@ -98,6 +100,10 @@ console.log('constructor') description: '', namespace: mailtrainConfig.user.namespace, type: mailtrainConfig.editors[0], + + fromSourceTemplate: false, + sourceTemplate: null, + text: '', html: '', data: {}, @@ -122,6 +128,12 @@ console.log('constructor') state.setIn(['type', 'error'], t('typeMustBeSelected')); } + if (state.getIn(['fromSourceTemplate', 'value']) && !state.getIn(['sourceTemplate', 'value'])) { + state.setIn(['sourceTemplate', 'error'], t('Source template must not be empty')); + } else { + state.setIn(['sourceTemplate', 'error'], null); + } + validateNamespace(t, state); if (typeKey) { @@ -255,6 +267,13 @@ console.log('constructor') typeForm = getTypeForm(this, typeKey, isEdit); } + const templatesColumns = [ + { data: 1, title: t('name') }, + { data: 2, title: t('description') }, + { data: 3, title: t('type'), render: data => this.templateTypes[data].typeName }, + { data: 4, title: t('created'), render: data => moment(data).fromNow() }, + { data: 5, title: t('namespace') }, + ]; return (
@@ -281,16 +300,25 @@ console.log('constructor')