Merge branch 'master' of github.com:Mailtrain-org/mailtrain into access
This commit is contained in:
commit
d13fc65ce2
32 changed files with 1190 additions and 295 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -26,3 +26,4 @@ public/grapejs/uploads/*
|
|||
!public/grapejs/uploads/README.md
|
||||
public/grapejs/templates/*
|
||||
!public/grapejs/templates/demo
|
||||
!public/grapejs/templates/aves
|
||||
|
|
18
.travis.yml
Normal file
18
.travis.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
dist: trusty
|
||||
sudo: required
|
||||
language: node_js
|
||||
node_js:
|
||||
- 7
|
||||
services:
|
||||
- mysql
|
||||
before_install:
|
||||
- sudo apt-get -q -y install pwgen imagemagick
|
||||
install:
|
||||
- sudo bash test/e2e/install.sh
|
||||
- npm install
|
||||
before_script:
|
||||
- npm run starttest > /dev/null 2>&1 &
|
||||
- sleep 10
|
||||
script:
|
||||
- grunt
|
||||
- npm run _e2e
|
|
@ -151,7 +151,10 @@ templates=[["versafix-1", "Versafix One"]]
|
|||
|
||||
[grapejs]
|
||||
# Installed templates
|
||||
templates=[["demo", "Demo Template"]]
|
||||
templates=[
|
||||
["demo", "HTML Template"],
|
||||
["aves", "MJML Template"]
|
||||
]
|
||||
|
||||
[reports]
|
||||
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
|
||||
|
|
20
lib/db.js
20
lib/db.js
|
@ -4,6 +4,8 @@ let config = require('config');
|
|||
let mysql = require('mysql');
|
||||
let redis = require('redis');
|
||||
let Lock = require('redfour');
|
||||
let stringifyDate = require('json-stringify-date');
|
||||
let tools = require('./tools');
|
||||
|
||||
module.exports = mysql.createPool(config.mysql);
|
||||
if (config.redis && config.redis.enabled) {
|
||||
|
@ -33,7 +35,7 @@ if (config.redis && config.redis.enabled) {
|
|||
};
|
||||
|
||||
module.exports.clearCache = (key, callback) => {
|
||||
module.exports.redis.del(key, err => callback(err));
|
||||
module.exports.redis.del('mailtrain:cache:' + key, err => callback(err));
|
||||
};
|
||||
|
||||
module.exports.addToCache = (key, value, callback) => {
|
||||
|
@ -41,7 +43,7 @@ if (config.redis && config.redis.enabled) {
|
|||
return setImmediate(() => callback());
|
||||
}
|
||||
module.exports.redis.multi().
|
||||
lpush('mailtrain:cache:' + key, JSON.stringify(value)).
|
||||
lpush('mailtrain:cache:' + key, stringifyDate.stringify(value)).
|
||||
expire('mailtrain:cache:' + key, 24 * 3600).
|
||||
exec(err => callback(err));
|
||||
};
|
||||
|
@ -52,7 +54,7 @@ if (config.redis && config.redis.enabled) {
|
|||
return callback(err);
|
||||
}
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
value = stringifyDate.parse(value);
|
||||
} catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
|
@ -76,9 +78,21 @@ if (config.redis && config.redis.enabled) {
|
|||
|
||||
module.exports.clearCache = (key, callback) => {
|
||||
caches.delete(key);
|
||||
tools.workers.forEach(child => {
|
||||
child.send({
|
||||
cmd: 'db.clearCache',
|
||||
key
|
||||
});
|
||||
});
|
||||
setImmediate(() => callback());
|
||||
};
|
||||
|
||||
process.on('message', m => {
|
||||
if (m && m.cmd === 'db.clearCache' && m.key) {
|
||||
caches.delete(m.key);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.addToCache = (key, value, callback) => {
|
||||
if (!caches.has(key)) {
|
||||
caches.set(key, []);
|
||||
|
|
|
@ -182,10 +182,10 @@ function createMailer(callback) {
|
|||
module.exports.transport.checkThrottling = null;
|
||||
}
|
||||
|
||||
let throttling = Number(configItems.smtpThrottling) || 0;
|
||||
if (throttling) {
|
||||
let sendingRate = Number(configItems.smtpThrottling) || 0;
|
||||
if (sendingRate) {
|
||||
// convert to messages/second
|
||||
throttling = 1 / (throttling / (3600 * 1000));
|
||||
sendingRate = sendingRate / 3600;
|
||||
}
|
||||
|
||||
let transportOptions;
|
||||
|
@ -236,7 +236,7 @@ function createMailer(callback) {
|
|||
error: logfunc.bind(null, 'error')
|
||||
},
|
||||
maxConnections: Number(configItems.smtpMaxConnections),
|
||||
sendingRate: throttling,
|
||||
sendingRate,
|
||||
tls: {
|
||||
rejectUnauthorized: !configItems.smtpSelfSigned
|
||||
}
|
||||
|
@ -257,19 +257,30 @@ function createMailer(callback) {
|
|||
oldListeners.forEach(listener => module.exports.transport.on('idle', listener));
|
||||
}
|
||||
|
||||
let lastCheck = Date.now();
|
||||
|
||||
if (configItems.mailTransport === 'smtp' || !configItems.mailTransport) {
|
||||
|
||||
let throttling = Number(configItems.smtpThrottling) || 0;
|
||||
if (throttling) {
|
||||
throttling = 1 / (throttling / (3600 * 1000));
|
||||
}
|
||||
|
||||
let lastCheck = Date.now();
|
||||
|
||||
module.exports.transport.checkThrottling = function (next) {
|
||||
if (!throttling) {
|
||||
return next();
|
||||
}
|
||||
let nextCheck = Date.now();
|
||||
let checkDiff = (nextCheck - lastCheck);
|
||||
lastCheck = nextCheck;
|
||||
if (checkDiff < throttling) {
|
||||
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
||||
setTimeout(next, throttling - checkDiff);
|
||||
setTimeout(() => {
|
||||
lastCheck = Date.now();
|
||||
next();
|
||||
}, throttling - checkDiff);
|
||||
} else {
|
||||
lastCheck = nextCheck;
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -458,7 +458,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
value: Number(valueList[field.column]) || 0,
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: Number(valueList[field.column]) || Number(field.defaultValue) || 0,
|
||||
mergeValue: (Number(valueList[field.column]) || Number(field.defaultValue) || 0).toString(),
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let striptags = require('striptags');
|
||||
let tools = require('../tools');
|
||||
let helpers = require('../helpers');
|
||||
let fields = require('./fields');
|
||||
|
@ -136,6 +137,8 @@ module.exports.insert = (listId, meta, subscriptionData, callback) => {
|
|||
values.push(field.value);
|
||||
});
|
||||
|
||||
values = values.map(v => typeof v === 'string' ? striptags(v) : v);
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -355,7 +358,7 @@ module.exports.getWithMergeTags = (listId, cid, callback) => {
|
|||
TIMEZONE: subscription.tz || ''
|
||||
};
|
||||
|
||||
fields.getRow(fieldList, subscription, true, true).forEach(field => {
|
||||
fields.getRow(fieldList, subscription, false, true).forEach(field => {
|
||||
if (field.mergeTag) {
|
||||
subscription.mergeTags[field.mergeTag] = field.mergeValue || '';
|
||||
}
|
||||
|
@ -420,6 +423,8 @@ module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
|||
return callback(null, false);
|
||||
}
|
||||
|
||||
values = values.map(v => typeof v === 'string' ? striptags(v) : v);
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
|
19
lib/tools.js
19
lib/tools.js
|
@ -13,6 +13,7 @@ let he = require('he');
|
|||
let _ = require('./translate')._;
|
||||
let util = require('util');
|
||||
let createDOMPurify = require('dompurify');
|
||||
let htmlToText = require('html-to-text');
|
||||
|
||||
let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www'];
|
||||
|
||||
|
@ -200,17 +201,24 @@ function formatMessage(serviceUrl, campaign, list, subscription, message, filter
|
|||
return links[key];
|
||||
}
|
||||
if (subscription.mergeTags.hasOwnProperty(key)) {
|
||||
return isHTML ? he.encode((subscription.mergeTags[key] || ''), {
|
||||
useNamedReferences: true
|
||||
}) : subscription.mergeTags[key];
|
||||
let value = (subscription.mergeTags[key] || '').toString();
|
||||
let containsHTML = /<[a-z][\s\S]*>/.test(value);
|
||||
return isHTML ? he.encode((containsHTML ? value : value.replace(/(?:\r\n|\r|\n)/g, '<br/>')), {
|
||||
useNamedReferences: true,
|
||||
allowUnsafeSymbols: true
|
||||
}) : (containsHTML ? htmlToText.fromString(value) : value);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
|
||||
identifier = identifier.toUpperCase();
|
||||
let value = (getValue(identifier) || fallback || '').trim();
|
||||
return value ? filter(value) : match;
|
||||
let value = getValue(identifier);
|
||||
if (value === false) {
|
||||
return match;
|
||||
}
|
||||
value = (value || fallback || '').trim();
|
||||
return filter(value);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -310,4 +318,3 @@ function mergeTemplateIntoLayout(template, layout, callback) {
|
|||
return done(template, layout);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
"isemail": "^2.2.1",
|
||||
"jquery-file-upload-middleware": "^0.1.8",
|
||||
"jsdom": "^9.12.0",
|
||||
"json-stringify-date": "^0.1.4",
|
||||
"juice": "^4.0.2",
|
||||
"knex": "^0.13.0",
|
||||
"libmime": "^3.1.0",
|
||||
|
|
2
public/grapejs/dist/css/grapes.min.css
vendored
2
public/grapejs/dist/css/grapes.min.css
vendored
File diff suppressed because one or more lines are too long
248
public/grapejs/dist/css/grapesjs-mjml.css
vendored
Normal file
248
public/grapejs/dist/css/grapesjs-mjml.css
vendored
Normal file
|
@ -0,0 +1,248 @@
|
|||
.gjs-clm-tags .gjs-sm-title,
|
||||
.gjs-sm-sector .gjs-sm-title {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.gjs-clm-tags .gjs-clm-tag {
|
||||
/* background-color: $tag-color; */
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 5px 8px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-field {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-btnt.gjs-pn-active,
|
||||
.gjs-pn-btn.gjs-pn-active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-pn-btn:hover {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.gjs-btnt.gjs-pn-active,
|
||||
.gjs-color-active,
|
||||
.gjs-pn-btn.gjs-pn-active,
|
||||
.gjs-pn-btn:active,
|
||||
.gjs-block:hover {
|
||||
color: #f45e43; /* #f46d4c,#e4505d */
|
||||
}
|
||||
|
||||
#gjs-rte-toolbar .gjs-rte-btn,
|
||||
.gjs-btn-prim,
|
||||
.gjs-btnt,
|
||||
.gjs-clm-tags .gjs-sm-composite.gjs-clm-field,
|
||||
.gjs-clm-tags .gjs-sm-field.gjs-sm-composite,
|
||||
.gjs-clm-tags .gjs-sm-stack #gjs-sm-add,
|
||||
.gjs-color-main,
|
||||
.gjs-mdl-dialog,
|
||||
.gjs-off-prv,
|
||||
.gjs-pn-btn,
|
||||
.gjs-pn-panel,
|
||||
.gjs-sm-sector .gjs-sm-composite.gjs-clm-field,
|
||||
.gjs-sm-sector .gjs-sm-field.gjs-sm-composite,
|
||||
.gjs-sm-sector .gjs-sm-stack #gjs-sm-add {
|
||||
color: #888686;
|
||||
}
|
||||
|
||||
#gjs-rte-toolbar,
|
||||
.gjs-bg-main,
|
||||
.gjs-clm-select option,
|
||||
.gjs-clm-tags .gjs-sm-colorp-c,
|
||||
.gjs-editor,
|
||||
.gjs-mdl-dialog,
|
||||
.gjs-nv-item .gjs-nv-title-c,
|
||||
.gjs-off-prv,
|
||||
.gjs-pn-panel,
|
||||
.gjs-select option,
|
||||
.gjs-sm-sector .gjs-sm-colorp-c,
|
||||
.gjs-sm-select option,
|
||||
.gjs-sm-unit option,
|
||||
.sp-container,
|
||||
.gjs-block {
|
||||
background-color: #2c2e35;
|
||||
}
|
||||
|
||||
.gjs-import-label,
|
||||
.gjs-export-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog .gjs-btn-import {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-radius: 3px;
|
||||
height: 450px;
|
||||
font-family: sans-serif, monospace;
|
||||
letter-spacing: 0.3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Extra */
|
||||
|
||||
.gjs-block {
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
margin: 10px 2.5% 5px;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow 0.2s ease 0s;
|
||||
}
|
||||
|
||||
.gjs-block:hover {
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
#gjs-pn-views-container.gjs-pn-panel {
|
||||
padding: 39px 0 0;
|
||||
}
|
||||
|
||||
#gjs-pn-views.gjs-pn-panel {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#gjs-pn-views .gjs-pn-btn {
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
width: 25%;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#gjs-pn-views .gjs-pn-active {
|
||||
/*
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border-bottom: 2px solid #f45e43;
|
||||
*/
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#gjs-pn-devices-c {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
#gjs-pn-options {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.gjs-sm-composite .gjs-sm-properties {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#gjs-sm-border-top-left-radius,
|
||||
#gjs-sm-border-top-right-radius,
|
||||
#gjs-sm-border-bottom-left-radius,
|
||||
#gjs-sm-border-bottom-right-radius,
|
||||
#gjs-sm-margin-top,
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-top,
|
||||
#gjs-sm-padding-bottom,
|
||||
#gjs-sm-padding-right,
|
||||
#gjs-sm-padding-left {
|
||||
flex: 999 1 60px;
|
||||
}
|
||||
|
||||
#gjs-sm-border-width,
|
||||
#gjs-sm-border-style,
|
||||
#gjs-sm-border-color {
|
||||
flex: 999 1 80px;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-left {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-padding-right {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-padding-bottom {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.gjs-field-radio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gjs-field-radio #gjs-sm-input-holder {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gjs-radio-item {
|
||||
flex: 1 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property.gjs-sm-list {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.gjs-mdl-content {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property .gjs-sm-layer.gjs-sm-active {
|
||||
background-color: rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
#gjs-pn-views-container,
|
||||
#gjs-pn-views{
|
||||
min-width: 270px;
|
||||
}
|
||||
*/
|
||||
|
||||
.gjs-f-button::before { content: 'B'; }
|
||||
.gjs-f-divider::before { content: 'D'; }
|
||||
|
||||
.gjs-mdl-dialog-sm {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-property {
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.anim-spin {
|
||||
animation: 0.5s linear 0s normal none infinite running spin;
|
||||
}
|
||||
|
||||
.form-status {
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f92929;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -54,14 +54,13 @@
|
|||
.gjs-nv-item .gjs-nv-title-c,
|
||||
.gjs-off-prv,
|
||||
.gjs-pn-panel,
|
||||
.gjs-block,
|
||||
.gjs-select option,
|
||||
.gjs-sm-sector .gjs-sm-colorp-c,
|
||||
.gjs-sm-select option,
|
||||
.gjs-sm-unit option,
|
||||
.sp-container,
|
||||
.gjs-block {
|
||||
background-color: #373d49;
|
||||
}
|
||||
.sp-container {
|
||||
background-color: #373d49; }
|
||||
|
||||
.gjs-import-label,
|
||||
.gjs-export-label {
|
||||
|
@ -79,18 +78,15 @@
|
|||
font-size: 12px; }
|
||||
|
||||
/* Extra */
|
||||
|
||||
.gjs-block {
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
margin: 10px 2.5% 5px;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
||||
transition: box-shadow 0.2s ease 0s;
|
||||
}
|
||||
transition: box-shadow, color 0.2s ease 0s; }
|
||||
|
||||
.gjs-block:hover {
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); }
|
||||
|
||||
#gjs-pn-views-container.gjs-pn-panel {
|
||||
padding: 39px 0 0; }
|
||||
|
|
34
public/grapejs/dist/js/grapes.min.js
vendored
34
public/grapejs/dist/js/grapes.min.js
vendored
File diff suppressed because one or more lines are too long
108
public/grapejs/dist/js/grapesjs-mjml.min.js
vendored
Normal file
108
public/grapejs/dist/js/grapesjs-mjml.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
124
public/grapejs/dist/js/grapesjs-preset-mjml.js
vendored
Normal file
124
public/grapejs/dist/js/grapesjs-preset-mjml.js
vendored
Normal file
|
@ -0,0 +1,124 @@
|
|||
grapesjs.plugins.add('gjs-preset-mjml', (editor, opts) => {
|
||||
var opt = opts || {};
|
||||
var config = editor.getConfig();
|
||||
|
||||
config.showDevices = 0;
|
||||
|
||||
var updateTooltip = function(coll, pos) {
|
||||
coll.each(function(item) {
|
||||
var attrs = item.get('attributes');
|
||||
attrs['data-tooltip-pos'] = pos || 'bottom';
|
||||
item.set('attributes', attrs);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/****************** COMMANDS *************************/
|
||||
|
||||
var cmdm = editor.Commands;
|
||||
cmdm.add('undo', {
|
||||
run: function(editor, sender) {
|
||||
sender.set('active', 0);
|
||||
editor.UndoManager.undo(1);
|
||||
}
|
||||
});
|
||||
cmdm.add('redo', {
|
||||
run: function(editor, sender) {
|
||||
sender.set('active', 0);
|
||||
editor.UndoManager.redo(1);
|
||||
}
|
||||
});
|
||||
cmdm.add('set-device-desktop', {
|
||||
run: function(editor) {
|
||||
editor.setDevice('Desktop');
|
||||
}
|
||||
});
|
||||
cmdm.add('set-device-mobile', {
|
||||
run: function(editor) {
|
||||
editor.setDevice('Mobile');
|
||||
}
|
||||
});
|
||||
cmdm.add('clean-all', {
|
||||
run: function(editor, sender) {
|
||||
sender && sender.set('active',false);
|
||||
if (confirm('Are you sure you want to clean the canvas?')) {
|
||||
editor.setComponents('<mj-container><mj-section><mj-column>'+
|
||||
'<mj-text>Start from here</mj-text></mj-column></mj-section></mj-container>');
|
||||
localStorage.setItem('gjs-mjml-css', '');
|
||||
localStorage.setItem('gjs-mjml-html', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/****************** BUTTONS *************************/
|
||||
|
||||
var pnm = editor.Panels;
|
||||
pnm.addButton('options', [{
|
||||
id: 'undo',
|
||||
className: 'fa fa-undo icon-undo',
|
||||
command: 'undo',
|
||||
attributes: { title: 'Undo (CTRL/CMD + Z)'}
|
||||
},{
|
||||
id: 'redo',
|
||||
className: 'fa fa-repeat icon-redo',
|
||||
command: 'redo',
|
||||
attributes: { title: 'Redo (CTRL/CMD + SHIFT + Z)' }
|
||||
},{
|
||||
id: 'clean-all',
|
||||
className: 'fa fa-trash icon-blank',
|
||||
command: 'clean-all',
|
||||
attributes: { title: 'Empty canvas' }
|
||||
}]);
|
||||
|
||||
// Add devices buttons
|
||||
var panelDevices = pnm.addPanel({id: 'devices-c'});
|
||||
var deviceBtns = panelDevices.get('buttons');
|
||||
deviceBtns.add([{
|
||||
id: 'deviceDesktop',
|
||||
command: 'set-device-desktop',
|
||||
className: 'fa fa-desktop',
|
||||
attributes: {'title': 'Desktop'},
|
||||
active: 1,
|
||||
},{
|
||||
id: 'deviceMobile',
|
||||
command: 'set-device-mobile',
|
||||
className: 'fa fa-mobile',
|
||||
attributes: {'title': 'Mobile'},
|
||||
}]);
|
||||
|
||||
// Remove preview and code button
|
||||
let prvBtn = pnm.addButton('options', 'preview');
|
||||
let optPanel = pnm.getPanel('options');
|
||||
let cmdBtns = optPanel.get('buttons');
|
||||
prvBtn && cmdBtns.remove(prvBtn);
|
||||
|
||||
updateTooltip(deviceBtns);
|
||||
updateTooltip(pnm.getPanel('options').get('buttons'));
|
||||
updateTooltip(pnm.getPanel('options').get('buttons'));
|
||||
updateTooltip(pnm.getPanel('views').get('buttons'));
|
||||
|
||||
|
||||
|
||||
/****************** EVENTS *************************/
|
||||
|
||||
// On component change show the Style Manager
|
||||
editor.on('change:selectedComponent', function() {
|
||||
var openLayersBtn = editor.Panels.getButton('views', 'open-layers');
|
||||
|
||||
// Don't switch when the Layer Manager is on or
|
||||
// there is no selected component
|
||||
if((!openLayersBtn || !openLayersBtn.get('active')) &&
|
||||
editor.editor.get('selectedComponent')) {
|
||||
var openSmBtn = editor.Panels.getButton('views', 'open-sm');
|
||||
openSmBtn && openSmBtn.set('active', 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Do stuff on load
|
||||
editor.on('load', function() {
|
||||
// Open block manager
|
||||
var openBlocksBtn = editor.Panels.getButton('views', 'open-blocks');
|
||||
openBlocksBtn && openBlocksBtn.set('active', 1);
|
||||
});
|
||||
|
||||
});
|
File diff suppressed because one or more lines are too long
1
public/grapejs/dist/js/toastr.min.js
vendored
1
public/grapejs/dist/js/toastr.min.js
vendored
File diff suppressed because one or more lines are too long
9
public/grapejs/templates/aves/CREDITS.md
Normal file
9
public/grapejs/templates/aves/CREDITS.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
#### Image Credits
|
||||
|
||||
All images CC0. Thanks to the Pixabay community.
|
||||
|
||||
* [bird-1.jpg](https://pixabay.com/de/zoo-pfau-kopf-tier-federn-vogel-866181/)
|
||||
* [bird-2.jpg](https://pixabay.com/de/möwe-himmel-urlaub-vogel-schnabel-249638/)
|
||||
* [bird-3.jpg](https://pixabay.com/de/porträt-vogel-natur-wild-räuber-1072696/)
|
||||
* [bird-4.jpg](https://pixabay.com/de/goldener-fasan-vogel-exotische-317503/)
|
||||
* [clouds.jpg](https://pixabay.com/de/luft-atmosphäre-blau-klar-klima-2716/)
|
BIN
public/grapejs/templates/aves/images/bird-1.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
BIN
public/grapejs/templates/aves/images/bird-2.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
public/grapejs/templates/aves/images/bird-3.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
BIN
public/grapejs/templates/aves/images/bird-4.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 145 KiB |
BIN
public/grapejs/templates/aves/images/clouds.jpg
Normal file
BIN
public/grapejs/templates/aves/images/clouds.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
86
public/grapejs/templates/aves/index.mjml
Normal file
86
public/grapejs/templates/aves/index.mjml
Normal file
|
@ -0,0 +1,86 @@
|
|||
<mjml>
|
||||
<mj-head>
|
||||
<mj-title>Hello World</mj-title>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-container>
|
||||
|
||||
<!-- Company Header -->
|
||||
<mj-section background-color="#f0f0f0">
|
||||
<mj-column>
|
||||
<mj-text font-style="italic" font-size="20" color="#626262">
|
||||
My Company
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<!-- Image Header -->
|
||||
<mj-section background-url="./images/clouds.jpg" background-size="cover" background-repeat="no-repeat">
|
||||
<mj-column>
|
||||
<mj-text align="center" color="#fff" font-size="40" font-family="Helvetica Neue" padding-top="30px" padding-bottom="30px">Slogan here</mj-text>
|
||||
<mj-button background-color="#F63A4D" href="#">
|
||||
Promotion
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<!-- Intro text -->
|
||||
<mj-section background-color="#fafafa">
|
||||
<mj-column width="400">
|
||||
<mj-text font-style="italic" font-size="20" font-family="Helvetica Neue" color="#cc2d2d">Attention!</mj-text>
|
||||
<mj-text color="#525252">
|
||||
The MJML Mode is currently experimental and there're a few bugs you should avoid.
|
||||
<ul>
|
||||
<li>Don't toggle visibility in the layer manager.</li>
|
||||
<li>Don't import the mj-head if you import a custom template.</li>
|
||||
<li>Don't duplicate mj-image as the duplicate is missing the src attribute on export. This bug is probably not limited to mj-image.</li>
|
||||
<li>Don't use % values.</li>
|
||||
</ul>
|
||||
It's generally working great and very promising. Thanks to @artf for making this all possible.
|
||||
</mj-text>
|
||||
<mj-button background-color="#F45E43" href="https://github.com/artf/grapesjs-mjml">Learn more about GrapesJS MJML</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<!-- Side image -->
|
||||
<mj-section background-color="white">
|
||||
<mj-column>
|
||||
<mj-image src="./images/bird-1.jpg" />
|
||||
</mj-column>
|
||||
<mj-column>
|
||||
<mj-text font-style="italic" font-size="20" font-family="Helvetica Neue" color="#626262">
|
||||
Amazing Birds
|
||||
</mj-text>
|
||||
<mj-text color="#525252">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<!-- Icons -->
|
||||
<mj-section background-color="#fbfbfb">
|
||||
<mj-column>
|
||||
<mj-image width="200" src="./images/bird-2.jpg" />
|
||||
</mj-column>
|
||||
<mj-column>
|
||||
<mj-image width="200" src="./images/bird-3.jpg" />
|
||||
</mj-column>
|
||||
<mj-column>
|
||||
<mj-image width="200" src="./images/bird-4.jpg" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<!-- Footer -->
|
||||
<mj-section background-color="#e7e7e7">
|
||||
<mj-column>
|
||||
<mj-text color="#525252">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus, sit amet suscipit nibh. Proin nec commodo purus. Sed eget
|
||||
nulla elit. Nulla aliquet mollis faucibus.
|
||||
</mj-text>
|
||||
<mj-button href="#">Hello There!</mj-button>
|
||||
<mj-social display="facebook twitter" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-container>
|
||||
</mj-body>
|
||||
</mjml>
|
|
@ -545,7 +545,7 @@ router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
|||
let campaignCid = campaign.cid;
|
||||
let listCid = list.cid;
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '`.`created', 'count'];
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
|
||||
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
|
@ -634,7 +634,7 @@ router.post('/status/ajax/:id/:status', (req, res) => {
|
|||
let campaignCid = campaign.cid;
|
||||
let listCid = list.cid;
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign__' + campaign.id + '`.`updated'];
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign__' + campaign.id + '.updated'];
|
||||
campaigns.filterStatusSubscribers(campaign, status, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
|
@ -683,7 +683,7 @@ router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
|||
let campaignCid = campaign.cid;
|
||||
let listCid = list.cid;
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '`.`created', 'count'];
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
|
||||
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let editorHelpers = require('../lib/editor-helpers.js')
|
||||
const config = require('config');
|
||||
const express = require('express');
|
||||
const router = new express.Router();
|
||||
const passport = require('../lib/passport');
|
||||
const _ = require('../lib/translate')._;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const editorHelpers = require('../lib/editor-helpers')
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
|
@ -23,28 +24,38 @@ router.get('/editor', passport.csrfProtection, (req, res) => {
|
|||
return res.redirect('/');
|
||||
}
|
||||
|
||||
resource.editorName = resource.editorName || 'grapejs';
|
||||
resource.editorData = !resource.editorData ?
|
||||
{
|
||||
try {
|
||||
resource.editorData = JSON.parse(resource.editorData);
|
||||
} catch (err) {
|
||||
resource.editorData = {
|
||||
template: req.query.template || 'demo'
|
||||
} :
|
||||
JSON.parse(resource.editorData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resource.html && !resource.editorData.html) {
|
||||
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
|
||||
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template);
|
||||
try {
|
||||
let file = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template, 'index.html');
|
||||
resource.html = fs.readFileSync(file, 'utf8');
|
||||
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
|
||||
} catch (err) {
|
||||
resource.html = err.message || err;
|
||||
try {
|
||||
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
|
||||
} catch (err) {
|
||||
resource.html = err.message || err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.render('grapejs/editor', {
|
||||
layout: 'grapejs/layout-editor',
|
||||
type: req.query.type,
|
||||
stringifiedResource: JSON.stringify(resource),
|
||||
resource,
|
||||
editorConfig: config.grapejs,
|
||||
csrfToken: req.csrfToken(),
|
||||
editor: {
|
||||
name: resource.editorName || 'grapejs',
|
||||
mode: resource.editorData.mjml ? 'mjml' : 'html',
|
||||
config: config.grapejs
|
||||
},
|
||||
csrfToken: req.csrfToken()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -257,7 +257,7 @@ router.post('/status/ajax/:id', (req, res) => {
|
|||
});
|
||||
}
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'trigger__' + trigger.id + '`.`created'];
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'trigger__' + trigger.id + '.created'];
|
||||
triggers.filterSubscribers(trigger, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
|
|
|
@ -332,7 +332,7 @@ function formatMessage(message, callback) {
|
|||
};
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, message.subscription, true, true).forEach(field => {
|
||||
fields.getRow(fieldList, message.subscription, false, true).forEach(field => {
|
||||
if (field.mergeTag) {
|
||||
message.subscription.mergeTags[field.mergeTag] = field.mergeValue || '';
|
||||
}
|
||||
|
@ -466,119 +466,129 @@ let sendLoop = () => {
|
|||
return setTimeout(sendLoop, 10 * 1000);
|
||||
}
|
||||
|
||||
let isThrottled = false;
|
||||
|
||||
let getNext = () => {
|
||||
if (!mailer.transport.isIdle()) {
|
||||
if (!mailer.transport.isIdle() || isThrottled) {
|
||||
// only retrieve new messages if there are free slots in the mailer queue
|
||||
return;
|
||||
}
|
||||
|
||||
// find an unsent message
|
||||
findUnsent((err, message) => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
setTimeout(getNext, mailing_timeout);
|
||||
return;
|
||||
}
|
||||
if (!message) {
|
||||
setTimeout(getNext, mailing_timeout);
|
||||
return;
|
||||
}
|
||||
isThrottled = true;
|
||||
|
||||
//log.verbose('Mail', 'Found new message to be delivered: %s', message.subscription.cid);
|
||||
// format message to nodemailer message format
|
||||
formatMessage(message, (err, mail) => {
|
||||
mailer.transport.checkThrottling(() => {
|
||||
|
||||
isThrottled = false;
|
||||
|
||||
// find an unsent message
|
||||
findUnsent((err, message) => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
setTimeout(getNext, mailing_timeout);
|
||||
return;
|
||||
}
|
||||
if (!message) {
|
||||
setTimeout(getNext, mailing_timeout);
|
||||
return;
|
||||
}
|
||||
|
||||
blacklist.isblacklisted(mail.to.address, (err, blacklisted) => {
|
||||
if (err) {
|
||||
log.error('Mail', err);
|
||||
setTimeout(getNext, mailing_timeout);
|
||||
return;
|
||||
}
|
||||
if (!blacklisted) {
|
||||
let tryCount = 0;
|
||||
let trySend = () => {
|
||||
tryCount++;
|
||||
// log.verbose('Mail', 'Found new message to be delivered: %s', message.subscription.cid);
|
||||
// format message to nodemailer message format
|
||||
formatMessage(message, (err, mail) => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
setTimeout(getNext, mailing_timeout);
|
||||
return;
|
||||
}
|
||||
|
||||
// send the message
|
||||
mailer.transport.sendMail(mail, (err, info) => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
||||
// temporary error, try again
|
||||
return setTimeout(trySend, tryCount * 1000);
|
||||
}
|
||||
}
|
||||
blacklist.isblacklisted(mail.to.address, (err, blacklisted) => {
|
||||
if (err) {
|
||||
log.error('Mail', err);
|
||||
setTimeout(getNext, mailing_timeout);
|
||||
return;
|
||||
}
|
||||
|
||||
let status = err ? 2 : 1;
|
||||
let response = err && (err.response || err.message) || info.response || info.messageId;
|
||||
let responseId = response.split(/\s+/).pop();
|
||||
if (!blacklisted) {
|
||||
let tryCount = 0;
|
||||
let trySend = () => {
|
||||
tryCount++;
|
||||
|
||||
// send the message
|
||||
mailer.transport.sendMail(mail, (err, info) => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
||||
// temporary error, try again
|
||||
return setTimeout(trySend, tryCount * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
let status = err ? 2 : 1;
|
||||
let response = err && (err.response || err.message) || info.response || info.messageId;
|
||||
let responseId = response.split(/\s+/).pop();
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
return;
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaigns` SET `delivered`=`delivered`+1 ' + (status === 2 ? ', `bounced`=`bounced`+1 ' : '') + ' WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, [message.campaignId], err => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, [status, response, responseId, message.id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
} else {
|
||||
// log.verbose('Mail', 'Message sent and status updated for %s', message.subscription.cid);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
setImmediate(trySend);
|
||||
} else {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
log.error('Mail', err);
|
||||
return;
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaigns` SET `delivered`=`delivered`+1 ' + (status === 2 ? ', `bounced`=`bounced`+1 ' : '') + ' WHERE id=? LIMIT 1';
|
||||
let query = 'UPDATE `campaigns` SET `blacklisted`=`blacklisted`+1 WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, [message.campaignId], err => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
log.error('Mail', err);
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, [status, response, responseId, message.id], err => {
|
||||
connection.query(query, [5, 'blacklisted', 'blacklisted', message.id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
log.error('Mail', err.stack);
|
||||
} else {
|
||||
// log.verbose('Mail', 'Message sent and status updated for %s', message.subscription.cid);
|
||||
log.error('Mail', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
setImmediate(trySend);
|
||||
} else {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
log.error('Mail', err);
|
||||
return;
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaigns` SET `blacklisted`=`blacklisted`+1 WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, [message.campaignId], err => {
|
||||
if (err) {
|
||||
log.error('Mail', err);
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, [5, 'blacklisted', 'blacklisted', message.id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
log.error('Mail', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
setImmediate(() => mailer.transport.checkThrottling(getNext));
|
||||
}
|
||||
setImmediate(getNext);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
mailer.transport.on('idle', () => mailer.transport.checkThrottling(getNext));
|
||||
setImmediate(() => mailer.transport.checkThrottling(getNext));
|
||||
mailer.transport.on('idle', getNext);
|
||||
setImmediate(getNext);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -436,4 +436,75 @@ suite('Subscription use-cases', () => {
|
|||
});
|
||||
});
|
||||
|
||||
useCase('A subscriber address can be changed to an address which has been previously unsubscribed. #222', async () => {
|
||||
const page = getPage(config.lists.l1);
|
||||
|
||||
const oldSubscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||
email: generateEmail(),
|
||||
firstName: 'old first name',
|
||||
lastName: 'old last name'
|
||||
});
|
||||
|
||||
await step('User clicks the unsubscribe button.', async () => {
|
||||
await page.mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||
});
|
||||
|
||||
await step('System shows a notice that confirms unsubscription.', async () => {
|
||||
await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('System sends an email that confirms unsubscription.', async () => {
|
||||
await page.mailUnsubscriptionConfirmed.fetchMail(oldSubscription.email);
|
||||
});
|
||||
|
||||
const newSubscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||
email: generateEmail(),
|
||||
firstName: 'new first name'
|
||||
});
|
||||
|
||||
await step('User clicks the manage subscription button.', async () => {
|
||||
await page.mailSubscriptionConfirmed.click('manageLink');
|
||||
});
|
||||
|
||||
await step('User clicks the change address button.', async () => {
|
||||
await page.webManage.click('manageAddressLink');
|
||||
});
|
||||
|
||||
await step('Systems shows a form to change email.', async () => {
|
||||
await page.webManageAddress.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('User fills in the email address of the original subscription and submits the form.', async () => {
|
||||
await page.webManageAddress.setValue('emailNewInput', oldSubscription.email);
|
||||
await page.webManageAddress.submit();
|
||||
});
|
||||
|
||||
await step('System goes back to the profile form and shows a flash notice that further instructions are in the email.', async () => {
|
||||
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||
await page.webManage.waitForFlash();
|
||||
expect(await page.webManage.getFlash()).to.contain('An email with further instructions has been sent to the provided address');
|
||||
});
|
||||
|
||||
await step('System sends an email with a link to confirm the address change.', async () => {
|
||||
await page.mailConfirmAddressChange.fetchMail(oldSubscription.email);
|
||||
});
|
||||
|
||||
await step('User clicks confirm subscription in the email', async () => {
|
||||
await page.mailConfirmAddressChange.click('confirmLink');
|
||||
});
|
||||
|
||||
await step('System shows the profile form with a flash notice that address has been changed. The form does not contain data from the old subscription.', async () => {
|
||||
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||
await page.webManage.waitForFlash();
|
||||
expect(await page.webManage.getFlash()).to.contain('Email address changed');
|
||||
expect(await page.webManage.getValue('emailInput')).to.equal(oldSubscription.email);
|
||||
expect(await page.webManage.getValue('firstNameInput')).to.equal(newSubscription.firstName);
|
||||
expect(await page.webManage.getValue('lastNameInput')).to.equal('');
|
||||
});
|
||||
|
||||
await step('System sends an email with subscription confirmation.', async () => {
|
||||
await page.mailSubscriptionConfirmed.fetchMail(oldSubscription.email);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -59,14 +59,7 @@
|
|||
|
||||
|
||||
<div id="gjs-wrapper">
|
||||
<div id="gjs" style="height:0px; overflow:hidden">
|
||||
{{#if resource.editorData.html}}
|
||||
<style>{{{resource.editorData.css}}}</style>
|
||||
{{{resource.editorData.html}}}
|
||||
{{else}}
|
||||
{{{resource.html}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div id="gjs" style="height: 0px; overflow: hidden"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -136,21 +129,69 @@
|
|||
<script>
|
||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
||||
|
||||
var editor = grapesjs.init({
|
||||
height: '100%',
|
||||
storageManager: {
|
||||
type: 'none'
|
||||
},
|
||||
assetManager: {
|
||||
assets: [],
|
||||
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}',
|
||||
uploadText: 'Drop images here or click to upload',
|
||||
},
|
||||
container : '#gjs',
|
||||
fromElement: true,
|
||||
plugins: ['gjs-preset-newsletter'],
|
||||
pluginsOpts: {
|
||||
'gjs-preset-newsletter': {
|
||||
var resource = {{{stringifiedResource}}};
|
||||
|
||||
var config = (function(mode) {
|
||||
var c = {
|
||||
clearOnRender: true,
|
||||
height: '100%',
|
||||
storageManager: {
|
||||
type: 'none'
|
||||
},
|
||||
assetManager: {
|
||||
assets: [],
|
||||
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}',
|
||||
uploadText: 'Drop images here or click to upload',
|
||||
},
|
||||
container : '#gjs',
|
||||
fromElement: false,
|
||||
plugins: [],
|
||||
pluginsOpts: {},
|
||||
};
|
||||
|
||||
if (mode === 'mjml') {
|
||||
var serializer = new XMLSerializer();
|
||||
var doc = new DOMParser().parseFromString(resource.editorData.mjml, 'text/xml');
|
||||
|
||||
// convert relative to absolute urls
|
||||
['mj-wrapper', 'mj-section', 'mj-navbar', 'mj-hero', 'mj-image'].forEach(function(tagName) {
|
||||
var serviceUrl = window.location.protocol + '//' + window.location.host + '/';
|
||||
var elements = doc.getElementsByTagName(tagName);
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
var node = elements[i];
|
||||
var attrName = tagName === 'mj-image' ? 'src' : 'background-url';
|
||||
var url = node.getAttribute(attrName);
|
||||
|
||||
if (url && url.substring(0, 2) === './') {
|
||||
var absoluteUrl = serviceUrl + 'grapejs/templates/' + resource.editorData.template + '/' + url.substring(2);
|
||||
node.setAttribute(attrName, absoluteUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var title = doc.getElementsByTagName('mj-title')[0];
|
||||
if (title) {
|
||||
title.textContent = resource.name;
|
||||
}
|
||||
|
||||
var head = doc.getElementsByTagName('mj-head')[0];
|
||||
var mjHead = head ? serializer.serializeToString(head) : '<mj-head></mj-head>';
|
||||
|
||||
var container = doc.getElementsByTagName('mj-container')[0];
|
||||
var mjContainer = container ? serializer.serializeToString(container) : '<mj-container></mj-container>';
|
||||
|
||||
c.plugins.push('gjs-mjml', 'gjs-preset-mjml');
|
||||
c.pluginsOpts['gjs-mjml'] = {
|
||||
preMjml: '<mjml>' + mjHead + '<mj-body>',
|
||||
postMjml: '</mj-body></mjml>',
|
||||
};
|
||||
c.components = mjContainer;
|
||||
}
|
||||
|
||||
if (mode === 'html') {
|
||||
c.plugins.push('gjs-preset-newsletter');
|
||||
c.pluginsOpts['gjs-preset-newsletter'] = {
|
||||
modalLabelImport: 'Paste all your code here below and click import',
|
||||
modalLabelExport: 'Copy the code and use it wherever you want',
|
||||
codeViewerTheme: 'material',
|
||||
|
@ -163,68 +204,161 @@
|
|||
margin: 0,
|
||||
padding: 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
c.components = resource.editorData.html || resource.html || '';
|
||||
c.style = resource.editorData.css || '';
|
||||
}
|
||||
});
|
||||
|
||||
$.getJSON('/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}', function(data) {
|
||||
return c;
|
||||
})('{{editor.mode}}');
|
||||
|
||||
var editor = grapesjs.init(config);
|
||||
|
||||
$.getJSON('/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}', function(data) {
|
||||
editor.AssetManager.add(data.files);
|
||||
});
|
||||
|
||||
function getPreparedHtml() {
|
||||
var imgs = [];
|
||||
$('.gjs-pn-buttons > .gjs-pn-btn.fa.fa-desktop').click();
|
||||
$('.gjs-editor > .gjs-cv-canvas > iframe.gjs-frame')
|
||||
.contents()
|
||||
.find('img')
|
||||
.each(function() {
|
||||
var src = $(this).attr('src');
|
||||
var s = src.match(/\/editorapi\/img\?src=([^&]*)/);
|
||||
var encodedSrc = (s && s[1]) || encodeURIComponent(src);
|
||||
var dynamicSrc = '/editorapi/img?src=' + encodedSrc + '&method=resize¶ms=' + $(this).width() + '%2C' + $(this).height();
|
||||
imgs.push({
|
||||
cls: $(this).attr('class').split(' ')[0],
|
||||
dynamicSrc: dynamicSrc,
|
||||
src: src,
|
||||
function getMjml() {
|
||||
var c = config.pluginsOpts['gjs-mjml'];
|
||||
return c.preMjml + editor.getHtml() + c.postMjml;
|
||||
}
|
||||
|
||||
function getPreparedHtml(callback) {
|
||||
var html;
|
||||
|
||||
switch ('{{editor.mode}}') {
|
||||
case 'html':
|
||||
html = editor.runCommand('gjs-get-inlined-html');
|
||||
html = '<!doctype html><html><head><meta charset="utf-8"><title>{{resource.name}}</title></head><body>' + html + '</body></html>';
|
||||
break;
|
||||
case 'mjml':
|
||||
var mjml = editor.runCommand('mjml-get-code');
|
||||
mjml.errors.length && mjml.errors.forEach(function(err) {
|
||||
console.warn(err.formattedMessage);
|
||||
});
|
||||
html = mjml.html;
|
||||
break;
|
||||
}
|
||||
|
||||
var frame = document.createElement('iframe');
|
||||
frame.width = 2048;
|
||||
frame.height = 0;
|
||||
document.body.appendChild(frame);
|
||||
var frameDoc = frame.contentDocument || frame.contentWindow.document;
|
||||
|
||||
frame.onload = function() {
|
||||
var imgs = frameDoc.querySelectorAll('img');
|
||||
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
var m = img.src.match(/\/editorapi\/img\?src=([^&]*)/);
|
||||
var encodedSrc = m && m[1] || encodeURIComponent(img.src);
|
||||
img.src = '/editorapi/img?src=' + encodedSrc + '&method=resize¶ms=' + img.clientWidth + '%2C' + img.clientHeight;
|
||||
}
|
||||
|
||||
html = '<!doctype html>' + frameDoc.documentElement.outerHTML;
|
||||
document.body.removeChild(frame);
|
||||
callback(html);
|
||||
};
|
||||
|
||||
frameDoc.open();
|
||||
frameDoc.write(html);
|
||||
frameDoc.close();
|
||||
}
|
||||
|
||||
|
||||
// Save Button
|
||||
|
||||
window.bridge = window.bridge || {};
|
||||
|
||||
$('#mt-save').on('click', function() {
|
||||
|
||||
if ($(this).hasClass('busy')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).addClass('busy');
|
||||
|
||||
getPreparedHtml(function(html) {
|
||||
var editorData = '{{editor.mode}}' === 'mjml' ? {
|
||||
template: resource.editorData.template,
|
||||
mjml: getMjml(),
|
||||
} : {
|
||||
template: resource.editorData.template,
|
||||
css: editor.getCss(),
|
||||
html: editor.getHtml(),
|
||||
style: editor.getStyle(),
|
||||
components: editor.getComponents(),
|
||||
};
|
||||
|
||||
// TODO: Make templates and campaigns accept partial updates, i.e. don't require 'name' and 'list'
|
||||
var update = {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
{{#if resource.list}} list: resource.list, {{/if}}
|
||||
editorData: JSON.stringify(editorData),
|
||||
html: html,
|
||||
};
|
||||
|
||||
$.post('/editorapi/update?type={{type}}&editor={{editor.name}}', update, null, 'html')
|
||||
.success(function() {
|
||||
window.bridge.lastSavedHtml = html;
|
||||
toastr.success('Sucessfully saved');
|
||||
})
|
||||
.fail(function(data) {
|
||||
toastr.error(data.responseText || 'An error occured while saving the document');
|
||||
})
|
||||
.always(function() {
|
||||
setTimeout(function() {
|
||||
$('#mt-save').removeClass('busy');
|
||||
}, 200); // Don't save too fast
|
||||
});
|
||||
});
|
||||
var html = editor.runCommand('gjs-get-inlined-html');
|
||||
imgs.forEach(function(img) {
|
||||
html = html.replace(
|
||||
'<img class="' + img.cls + '" src="' + img.src,
|
||||
'<img class="' + img.cls + '" src="' + img.dynamicSrc
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
html = '<!doctype html><html><head><meta charset="utf-8"><title>{{resource.name}}</title></head><body>' + html + '</body></html>';
|
||||
|
||||
return html;
|
||||
}
|
||||
// Close Button
|
||||
|
||||
$('#mt-close').on('click', function() {
|
||||
if (confirm('Unsaved changes will be lost. Close now?') === true) {
|
||||
window.bridge.exit
|
||||
? window.bridge.exit()
|
||||
: window.location.href = '/{{type}}s/edit/{{resource.id}}?tab=template';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Commands
|
||||
|
||||
var mdlClass = 'gjs-mdl-dialog-sm';
|
||||
var pnm = editor.Panels;
|
||||
var cmdm = editor.Commands;
|
||||
var testContainer = document.getElementById("test-form");
|
||||
var contentEl = testContainer.querySelector('input[name=html]');
|
||||
var md = editor.Modal;
|
||||
|
||||
|
||||
// Test email command
|
||||
|
||||
var testContainer = document.getElementById('test-form');
|
||||
var testContentEl = testContainer.querySelector('input[name=html]');
|
||||
|
||||
cmdm.add('send-test', {
|
||||
run(editor, sender) {
|
||||
sender.set('active', 0);
|
||||
var modalContent = md.getContentEl();
|
||||
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
||||
// var cmdGetCode = cmdm.get('gjs-get-inlined-html');
|
||||
// contentEl.value = cmdGetCode && cmdGetCode.run(editor);
|
||||
contentEl.value = getPreparedHtml();
|
||||
mdlDialog.className += ' ' + mdlClass;
|
||||
testContainer.style.display = 'block';
|
||||
md.setTitle('Test your Newsletter');
|
||||
md.setContent(testContainer);
|
||||
md.open();
|
||||
md.getModel().once('change:open', function() {
|
||||
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
|
||||
//clean status
|
||||
})
|
||||
// TODO: Show a spinner
|
||||
getPreparedHtml(function(html) {
|
||||
sender.set('active', 0);
|
||||
var modalContent = md.getContentEl();
|
||||
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
||||
testContentEl.value = html;
|
||||
mdlDialog.className += ' ' + mdlClass;
|
||||
testContainer.style.display = 'block';
|
||||
md.setTitle('Test your Newsletter');
|
||||
md.setContent(testContainer);
|
||||
md.open();
|
||||
md.getModel().once('change:open', function() {
|
||||
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
|
||||
//clean status
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -240,6 +374,7 @@
|
|||
|
||||
var statusFormElC = document.querySelector('.form-status');
|
||||
var statusFormEl = document.querySelector('.form-status i');
|
||||
|
||||
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
|
||||
.onStart(function() {
|
||||
statusFormEl.className = 'fa fa-refresh anim-spin';
|
||||
|
@ -260,7 +395,24 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Add Merge Tag Reference command
|
||||
// Remember testemail address
|
||||
|
||||
var isValidEmail = function(email) {
|
||||
return /\S+@\S+\.\S+/.test(email);
|
||||
};
|
||||
|
||||
if (isValidEmail(localStorage.getItem('testemail'))) {
|
||||
$('#test-form input[name=email]').val(localStorage.getItem('testemail'));
|
||||
}
|
||||
|
||||
$('#test-form').on('submit', function() {
|
||||
var email = $('#test-form input[name=email]').val();
|
||||
isValidEmail(email) && localStorage.setItem('testemail', email);
|
||||
});
|
||||
|
||||
|
||||
// Merge Tag Reference command
|
||||
|
||||
var mergeTagReferenceContainer = document.getElementById('merge-tag-reference-container');
|
||||
cmdm.add('open-merge-tag-reference', {
|
||||
run(editor, sender) {
|
||||
|
@ -287,7 +439,9 @@
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
// Simple warn notifier
|
||||
|
||||
var origWarn = console.warn;
|
||||
toastr.options = {
|
||||
closeButton: true,
|
||||
|
@ -300,8 +454,10 @@
|
|||
origWarn(msg);
|
||||
};
|
||||
|
||||
|
||||
// Beautify tooltips
|
||||
|
||||
$(document).ready(function() {
|
||||
// Beautify tooltips
|
||||
$('*[title]').each(function() {
|
||||
var el = $(this);
|
||||
var title = el.attr('title').trim();
|
||||
|
@ -310,65 +466,6 @@
|
|||
el.attr('title', '');
|
||||
}
|
||||
});
|
||||
|
||||
// Remember testmail address
|
||||
var isValidEmail = function(email) {
|
||||
return /\S+@\S+\.\S+/.test(email);
|
||||
};
|
||||
var email = localStorage.getItem('testemail');
|
||||
isValidEmail(email) && $('#test-form input[name=email]').val(email);
|
||||
|
||||
$(document).on('submit', '#test-form', function() {
|
||||
var email = $('#test-form input[name=email]').val();
|
||||
isValidEmail(email) && localStorage.setItem('testemail', email);
|
||||
});
|
||||
|
||||
|
||||
// Save and Close Buttons
|
||||
|
||||
window.bridge = window.bridge || {};
|
||||
|
||||
$('#mt-close').on('click', function() {
|
||||
if (confirm('Unsaved changes will be lost. Close now?') === true) {
|
||||
window.bridge.exit
|
||||
? window.bridge.exit()
|
||||
: window.location.href = '/{{type}}s/edit/{{resource.id}}?tab=template';
|
||||
}
|
||||
});
|
||||
|
||||
$('#mt-save').on('click', function() {
|
||||
if ($(this).hasClass('busy')) {
|
||||
return;
|
||||
}
|
||||
$(this).addClass('busy');
|
||||
|
||||
var html = getPreparedHtml();
|
||||
|
||||
$.post('/editorapi/update?type={{type}}&editor={{resource.editorName}}', {
|
||||
id: {{resource.id}},
|
||||
name: '{{resource.name}}',
|
||||
{{#if resource.list}} list: {{resource.list}}, {{/if}}
|
||||
html: html,
|
||||
editorData: JSON.stringify({
|
||||
template: '{{resource.editorData.template}}',
|
||||
css: editor.getCss(),
|
||||
html: editor.getHtml(),
|
||||
style: editor.getStyle(),
|
||||
components: editor.getComponents(),
|
||||
}),
|
||||
}, null, 'html')
|
||||
.success(function() {
|
||||
window.bridge.lastSavedHtml = html;
|
||||
toastr.success('Sucessfully saved');
|
||||
})
|
||||
.fail(function(data) {
|
||||
toastr.error(data.responseText || 'An error occured while saving the document');
|
||||
})
|
||||
.always(function() {
|
||||
setTimeout(function() {
|
||||
$(this).removeClass('busy');
|
||||
}.bind(this), 500); // Don't save too fast
|
||||
}.bind(this));
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -3,17 +3,28 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GrapesJS Newsletter Editor</title>
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css">
|
||||
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css?v=0.5.41">
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/toastr.min.css?v=2.1.3">
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/material.css">
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/tooltip.css">
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/toastr.min.css">
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-preset-newsletter.css">
|
||||
|
||||
<script src="/javascript/jquery-2.2.1.min.js"></script>
|
||||
<script src="/grapejs/dist/js/grapes.min.js"></script>
|
||||
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js"></script>
|
||||
<script src="/grapejs/dist/js/toastr.min.js"></script>
|
||||
<script src="/grapejs/dist/js/ajaxable.min.js"></script>
|
||||
<script src="/grapejs/dist/js/grapes.min.js?v=0.5.41"></script>
|
||||
<script src="/grapejs/dist/js/toastr.min.js?v=2.1.3"></script>
|
||||
<script src="/grapejs/dist/js/ajaxable.min.js?v=0.2.3"></script>
|
||||
|
||||
{{#switch editor.mode}}
|
||||
{{#case "mjml"}}
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-mjml.css?v=0.0.7">
|
||||
<script src="/grapejs/dist/js/grapesjs-mjml.min.js?v=0.0.7"></script>
|
||||
<script src="/grapejs/dist/js/grapesjs-preset-mjml.js"></script>
|
||||
{{/case}}
|
||||
{{#case "html"}}
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-preset-newsletter.css?v=0.2.3">
|
||||
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js?v=0.2.3"></script>
|
||||
{{/case}}
|
||||
{{/switch}}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
Loading…
Reference in a new issue