Merge branch 'master' of github.com:Mailtrain-org/mailtrain
This commit is contained in:
commit
6f554038c9
61 changed files with 2497 additions and 707 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
||||||
|
/.idea
|
||||||
|
/last-failed-e2e-test.*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -23,3 +26,4 @@ public/grapejs/uploads/*
|
||||||
!public/grapejs/uploads/README.md
|
!public/grapejs/uploads/README.md
|
||||||
public/grapejs/templates/*
|
public/grapejs/templates/*
|
||||||
!public/grapejs/templates/demo
|
!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
|
|
@ -1,6 +1,6 @@
|
||||||
# Mailtrain
|
# Mailtrain
|
||||||
|
|
||||||
[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v5+) and MySQL (v5.5+ or MariaDB).
|
[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v7+) and MySQL (v5.5+ or MariaDB).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ Check out [ZoneMTA](https://github.com/zone-eu/zone-mta) as an alternative self
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* Nodejs v6+
|
* Nodejs v7+
|
||||||
* MySQL v5.5 or MariaDB
|
* MySQL v5.5 or MariaDB
|
||||||
* Redis. Optional, disabled by default. Used for session storage and for caching state between multiple processes. If you do not have Redis enabled then you can only use a single sender process
|
* Redis. Optional, disabled by default. Used for session storage and for caching state between multiple processes. If you do not have Redis enabled then you can only use a single sender process
|
||||||
|
|
||||||
|
|
5
app.js
5
app.js
|
@ -184,8 +184,9 @@ app.use((req, res, next) => {
|
||||||
res.locals.customScripts = config.customscripts || [];
|
res.locals.customScripts = config.customscripts || [];
|
||||||
|
|
||||||
let bodyClasses = [];
|
let bodyClasses = [];
|
||||||
app.get('env') === 'test' && bodyClasses.push('page--' + (req.path.substring(1).replace(/\//g, '--') || 'home'));
|
if (req.user) {
|
||||||
req.user && bodyClasses.push('logged-in user-' + req.user.username);
|
bodyClasses.push('logged-in user-' + req.user.username);
|
||||||
|
}
|
||||||
res.locals.bodyClass = bodyClasses.join(' ');
|
res.locals.bodyClass = bodyClasses.join(' ');
|
||||||
|
|
||||||
settingsModel.list(['ua_code', 'shoutout'], (err, configItems) => {
|
settingsModel.list(['ua_code', 'shoutout'], (err, configItems) => {
|
||||||
|
|
|
@ -151,7 +151,10 @@ templates=[["versafix-1", "Versafix One"]]
|
||||||
|
|
||||||
[grapejs]
|
[grapejs]
|
||||||
# Installed templates
|
# Installed templates
|
||||||
templates=[["demo", "Demo Template"]]
|
templates=[
|
||||||
|
["demo", "HTML Template"],
|
||||||
|
["aves", "MJML Template"]
|
||||||
|
]
|
||||||
|
|
||||||
[reports]
|
[reports]
|
||||||
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
|
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
|
||||||
|
|
15
lib/db.js
15
lib/db.js
|
@ -4,6 +4,7 @@ let config = require('config');
|
||||||
let mysql = require('mysql');
|
let mysql = require('mysql');
|
||||||
let redis = require('redis');
|
let redis = require('redis');
|
||||||
let Lock = require('redfour');
|
let Lock = require('redfour');
|
||||||
|
let tools = require('./tools');
|
||||||
|
|
||||||
module.exports = mysql.createPool(config.mysql);
|
module.exports = mysql.createPool(config.mysql);
|
||||||
if (config.redis && config.redis.enabled) {
|
if (config.redis && config.redis.enabled) {
|
||||||
|
@ -33,7 +34,7 @@ if (config.redis && config.redis.enabled) {
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.clearCache = (key, callback) => {
|
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) => {
|
module.exports.addToCache = (key, value, callback) => {
|
||||||
|
@ -76,9 +77,21 @@ if (config.redis && config.redis.enabled) {
|
||||||
|
|
||||||
module.exports.clearCache = (key, callback) => {
|
module.exports.clearCache = (key, callback) => {
|
||||||
caches.delete(key);
|
caches.delete(key);
|
||||||
|
tools.workers.forEach(child => {
|
||||||
|
child.send({
|
||||||
|
cmd: 'db.clearCache',
|
||||||
|
key
|
||||||
|
});
|
||||||
|
});
|
||||||
setImmediate(() => callback());
|
setImmediate(() => callback());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
process.on('message', m => {
|
||||||
|
if (m && m.cmd === 'db.clearCache' && m.key) {
|
||||||
|
caches.delete(m.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports.addToCache = (key, value, callback) => {
|
module.exports.addToCache = (key, value, callback) => {
|
||||||
if (!caches.has(key)) {
|
if (!caches.has(key)) {
|
||||||
caches.set(key, []);
|
caches.set(key, []);
|
||||||
|
|
|
@ -182,10 +182,10 @@ function createMailer(callback) {
|
||||||
module.exports.transport.checkThrottling = null;
|
module.exports.transport.checkThrottling = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let throttling = Number(configItems.smtpThrottling) || 0;
|
let sendingRate = Number(configItems.smtpThrottling) || 0;
|
||||||
if (throttling) {
|
if (sendingRate) {
|
||||||
// convert to messages/second
|
// convert to messages/second
|
||||||
throttling = 1 / (throttling / (3600 * 1000));
|
sendingRate = sendingRate / 3600;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transportOptions;
|
let transportOptions;
|
||||||
|
@ -236,7 +236,7 @@ function createMailer(callback) {
|
||||||
error: logfunc.bind(null, 'error')
|
error: logfunc.bind(null, 'error')
|
||||||
},
|
},
|
||||||
maxConnections: Number(configItems.smtpMaxConnections),
|
maxConnections: Number(configItems.smtpMaxConnections),
|
||||||
sendingRate: throttling,
|
sendingRate,
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: !configItems.smtpSelfSigned
|
rejectUnauthorized: !configItems.smtpSelfSigned
|
||||||
}
|
}
|
||||||
|
@ -257,19 +257,30 @@ function createMailer(callback) {
|
||||||
oldListeners.forEach(listener => module.exports.transport.on('idle', listener));
|
oldListeners.forEach(listener => module.exports.transport.on('idle', listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastCheck = Date.now();
|
|
||||||
if (configItems.mailTransport === 'smtp' || !configItems.mailTransport) {
|
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) {
|
module.exports.transport.checkThrottling = function (next) {
|
||||||
if (!throttling) {
|
if (!throttling) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
let nextCheck = Date.now();
|
let nextCheck = Date.now();
|
||||||
let checkDiff = (nextCheck - lastCheck);
|
let checkDiff = (nextCheck - lastCheck);
|
||||||
lastCheck = nextCheck;
|
|
||||||
if (checkDiff < throttling) {
|
if (checkDiff < throttling) {
|
||||||
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
||||||
setTimeout(next, throttling - checkDiff);
|
setTimeout(() => {
|
||||||
|
lastCheck = Date.now();
|
||||||
|
next();
|
||||||
|
}, throttling - checkDiff);
|
||||||
} else {
|
} else {
|
||||||
|
lastCheck = nextCheck;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ let _ = require('../translate')._;
|
||||||
let util = require('util');
|
let util = require('util');
|
||||||
let tableHelpers = require('../table-helpers');
|
let tableHelpers = require('../table-helpers');
|
||||||
|
|
||||||
let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'tracking_disabled'];
|
let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'click_tracking_disabled', 'open_tracking_disabled'];
|
||||||
|
|
||||||
module.exports.list = (start, limit, callback) => {
|
module.exports.list = (start, limit, callback) => {
|
||||||
tableHelpers.list('campaigns', ['*'], 'scheduled', null, start, limit, callback);
|
tableHelpers.list('campaigns', ['*'], 'scheduled', null, start, limit, callback);
|
||||||
|
@ -370,7 +370,8 @@ module.exports.create = (campaign, opts, callback) => {
|
||||||
campaign = tools.convertKeys(campaign);
|
campaign = tools.convertKeys(campaign);
|
||||||
let name = (campaign.name || '').toString().trim();
|
let name = (campaign.name || '').toString().trim();
|
||||||
|
|
||||||
campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0;
|
campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0;
|
||||||
|
campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0;
|
||||||
|
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
|
||||||
|
@ -592,7 +593,8 @@ module.exports.update = (id, updates, callback) => {
|
||||||
let campaign = tools.convertKeys(updates);
|
let campaign = tools.convertKeys(updates);
|
||||||
let name = (campaign.name || '').toString().trim();
|
let name = (campaign.name || '').toString().trim();
|
||||||
|
|
||||||
campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0;
|
campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0;
|
||||||
|
campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return callback(new Error(_('Campaign Name must be set')));
|
return callback(new Error(_('Campaign Name must be set')));
|
||||||
|
@ -827,7 +829,7 @@ module.exports.reset = (id, callback) => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0 WHERE id=? LIMIT 1', [id], err => {
|
connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0, `blacklisted`=0 WHERE id=? LIMIT 1', [id], err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
connection.release();
|
connection.release();
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
|
|
@ -42,7 +42,7 @@ module.exports.countClick = (remoteIp, useragent, campaignCid, listCid, subscrip
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || data.campaign.trackingDisabled) {
|
if (!data || data.campaign.clickTrackingDisabled) {
|
||||||
return callback(null, false);
|
return callback(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ module.exports.countOpen = (remoteIp, useragent, campaignCid, listCid, subscript
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || data.campaign.trackingDisabled) {
|
if (!data || data.campaign.openTrackingDisabled) {
|
||||||
return callback(null, false);
|
return callback(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,20 +268,13 @@ module.exports.add = (url, campaignId, callback) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, callback) => {
|
module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, callback) => {
|
||||||
if (campaign.trackingDisabled || !message || !message.trim()) {
|
if ((campaign.openTrackingDisabled && campaign.clickTrackingDisabled) || !message || !message.trim()) {
|
||||||
// tracking is disabled, do not modify the message
|
// tracking is disabled, do not modify the message
|
||||||
return setImmediate(() => callback(null, message));
|
return setImmediate(() => callback(null, message));
|
||||||
}
|
}
|
||||||
let re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi;
|
|
||||||
let urls = new Set();
|
|
||||||
(message || '').replace(re, (match, prefix, url) => {
|
|
||||||
urls.add(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
let map = new Map();
|
|
||||||
let vals = urls.values();
|
|
||||||
|
|
||||||
// insert tracking image
|
// insert tracking image
|
||||||
|
if (!campaign.openTrackingDisabled) {
|
||||||
let inserted = false;
|
let inserted = false;
|
||||||
let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid)));
|
let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid)));
|
||||||
let img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">';
|
let img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">';
|
||||||
|
@ -292,6 +285,20 @@ module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message,
|
||||||
if (!inserted) {
|
if (!inserted) {
|
||||||
message = message + img;
|
message = message + img;
|
||||||
}
|
}
|
||||||
|
if (campaign.clickTrackingDisabled) {
|
||||||
|
return callback(null, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!campaign.clickTrackingDisabled) {
|
||||||
|
let re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi;
|
||||||
|
let urls = new Set();
|
||||||
|
(message || '').replace(re, (match, prefix, url) => {
|
||||||
|
urls.add(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
let map = new Map();
|
||||||
|
let vals = urls.values();
|
||||||
|
|
||||||
let replaceUrls = () => {
|
let replaceUrls = () => {
|
||||||
callback(null,
|
callback(null,
|
||||||
|
@ -318,6 +325,7 @@ module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message,
|
||||||
};
|
};
|
||||||
|
|
||||||
storeNext();
|
storeNext();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSubscriptionData(campaignCid, listCid, subscriptionCid, callback) {
|
function getSubscriptionData(campaignCid, listCid, subscriptionCid, callback) {
|
||||||
|
|
|
@ -148,7 +148,6 @@ function updateMenu(res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateEmail(address, checkBlocked, callback) {
|
function validateEmail(address, checkBlocked, callback) {
|
||||||
|
|
||||||
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
||||||
return callback(new Error(util.format(_('Blocked email address "%s"'), address)));
|
return callback(new Error(util.format(_('Blocked email address "%s"'), address)));
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 28
|
"schemaVersion": 29
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"sqldumptest": "NODE_ENV=test DUMP_NAME_SUFFIX=-test npm run sqldump",
|
"sqldumptest": "NODE_ENV=test DUMP_NAME_SUFFIX=-test npm run sqldump",
|
||||||
"sqlresettest": "NODE_ENV=test npm run sqldrop && NODE_ENV=test npm run sqlinit",
|
"sqlresettest": "NODE_ENV=test npm run sqldrop && NODE_ENV=test npm run sqlinit",
|
||||||
"starttest": "NODE_ENV=test node index.js",
|
"starttest": "NODE_ENV=test node index.js",
|
||||||
"_e2e": "NODE_ENV=test mocha test/e2e/index.js",
|
"_e2e": "NODE_ENV=test node test/e2e/index.js",
|
||||||
"e2e": "npm run sqlresettest && npm run _e2e"
|
"e2e": "npm run sqlresettest && npm run _e2e"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -39,10 +39,10 @@
|
||||||
"grunt-contrib-nodeunit": "^1.0.0",
|
"grunt-contrib-nodeunit": "^1.0.0",
|
||||||
"grunt-eslint": "^19.0.0",
|
"grunt-eslint": "^19.0.0",
|
||||||
"jsxgettext-andris": "^0.9.0-patch.1",
|
"jsxgettext-andris": "^0.9.0-patch.1",
|
||||||
"mailparser": "^2.0.5",
|
|
||||||
"mocha": "^3.3.0",
|
"mocha": "^3.3.0",
|
||||||
"phantomjs": "^2.1.7",
|
"phantomjs": "^2.1.7",
|
||||||
"selenium-webdriver": "^3.4.0"
|
"selenium-webdriver": "^3.4.0",
|
||||||
|
"url-pattern": "^1.0.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"posix": "^4.1.1"
|
"posix": "^4.1.1"
|
||||||
|
@ -70,6 +70,7 @@
|
||||||
"faker": "^4.1.0",
|
"faker": "^4.1.0",
|
||||||
"feedparser": "^2.1.0",
|
"feedparser": "^2.1.0",
|
||||||
"file-type": "^4.1.0",
|
"file-type": "^4.1.0",
|
||||||
|
"fs-extra": "^3.0.1",
|
||||||
"geoip-ultralight": "^0.1.5",
|
"geoip-ultralight": "^0.1.5",
|
||||||
"gettext-parser": "^1.2.2",
|
"gettext-parser": "^1.2.2",
|
||||||
"gm": "^1.23.0",
|
"gm": "^1.23.0",
|
||||||
|
@ -84,6 +85,7 @@
|
||||||
"jsdom": "^9.12.0",
|
"jsdom": "^9.12.0",
|
||||||
"juice": "^4.0.2",
|
"juice": "^4.0.2",
|
||||||
"libmime": "^3.1.0",
|
"libmime": "^3.1.0",
|
||||||
|
"mailparser": "^2.0.5",
|
||||||
"marked": "^0.3.6",
|
"marked": "^0.3.6",
|
||||||
"memory-cache": "^0.1.6",
|
"memory-cache": "^0.1.6",
|
||||||
"mjml": "3.3.0",
|
"mjml": "3.3.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-nv-item .gjs-nv-title-c,
|
||||||
.gjs-off-prv,
|
.gjs-off-prv,
|
||||||
.gjs-pn-panel,
|
.gjs-pn-panel,
|
||||||
|
.gjs-block,
|
||||||
.gjs-select option,
|
.gjs-select option,
|
||||||
.gjs-sm-sector .gjs-sm-colorp-c,
|
.gjs-sm-sector .gjs-sm-colorp-c,
|
||||||
.gjs-sm-select option,
|
.gjs-sm-select option,
|
||||||
.gjs-sm-unit option,
|
.gjs-sm-unit option,
|
||||||
.sp-container,
|
.sp-container {
|
||||||
.gjs-block {
|
background-color: #373d49; }
|
||||||
background-color: #373d49;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gjs-import-label,
|
.gjs-import-label,
|
||||||
.gjs-export-label {
|
.gjs-export-label {
|
||||||
|
@ -79,18 +78,15 @@
|
||||||
font-size: 12px; }
|
font-size: 12px; }
|
||||||
|
|
||||||
/* Extra */
|
/* Extra */
|
||||||
|
|
||||||
.gjs-block {
|
.gjs-block {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin: 10px 2.5% 5px;
|
margin: 10px 2.5% 5px;
|
||||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
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 {
|
.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 {
|
#gjs-pn-views-container.gjs-pn-panel {
|
||||||
padding: 39px 0 0; }
|
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>
|
|
@ -1,12 +1,13 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let config = require('config');
|
const config = require('config');
|
||||||
let express = require('express');
|
const express = require('express');
|
||||||
let router = new express.Router();
|
const router = new express.Router();
|
||||||
let passport = require('../lib/passport');
|
const passport = require('../lib/passport');
|
||||||
let fs = require('fs');
|
const _ = require('../lib/translate')._;
|
||||||
let path = require('path');
|
const fs = require('fs');
|
||||||
let editorHelpers = require('../lib/editor-helpers.js')
|
const path = require('path');
|
||||||
|
const editorHelpers = require('../lib/editor-helpers')
|
||||||
|
|
||||||
router.all('/*', (req, res, next) => {
|
router.all('/*', (req, res, next) => {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
|
@ -23,28 +24,38 @@ router.get('/editor', passport.csrfProtection, (req, res) => {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
resource.editorName = resource.editorName || 'grapejs';
|
|
||||||
resource.editorData = !resource.editorData ?
|
|
||||||
{
|
|
||||||
template: req.query.template || 'demo'
|
|
||||||
} :
|
|
||||||
JSON.parse(resource.editorData);
|
|
||||||
|
|
||||||
if (!resource.html && !resource.editorData.html) {
|
|
||||||
try {
|
try {
|
||||||
let file = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template, 'index.html');
|
resource.editorData = JSON.parse(resource.editorData);
|
||||||
resource.html = fs.readFileSync(file, 'utf8');
|
} catch (err) {
|
||||||
|
resource.editorData = {
|
||||||
|
template: req.query.template || 'demo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
|
||||||
|
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template);
|
||||||
|
try {
|
||||||
|
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
try {
|
||||||
|
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resource.html = err.message || err;
|
resource.html = err.message || err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.render('grapejs/editor', {
|
res.render('grapejs/editor', {
|
||||||
layout: 'grapejs/layout-editor',
|
layout: 'grapejs/layout-editor',
|
||||||
type: req.query.type,
|
type: req.query.type,
|
||||||
|
stringifiedResource: JSON.stringify(resource),
|
||||||
resource,
|
resource,
|
||||||
editorConfig: config.grapejs,
|
editor: {
|
||||||
csrfToken: req.csrfToken(),
|
name: resource.editorName || 'grapejs',
|
||||||
|
mode: resource.editorData.mjml ? 'mjml' : 'html',
|
||||||
|
config: config.grapejs
|
||||||
|
},
|
||||||
|
csrfToken: req.csrfToken()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -702,7 +702,12 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (req.query.formTest ||
|
const autoUnsubscribe = req.query.auto === 'yes';
|
||||||
|
|
||||||
|
if (autoUnsubscribe) {
|
||||||
|
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||||
|
|
||||||
|
} else if (req.query.formTest ||
|
||||||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
||||||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||||
|
|
||||||
|
@ -741,7 +746,7 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
|
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
|
||||||
handleUnsubscribe(list, subscription, req.query.c, req.ip, res, next);
|
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -771,14 +776,32 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsubscribe(list, subscription, campaignId, req.ip, res, next);
|
handleUnsubscribe(list, subscription, false, campaignId, req.ip, res, next);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleUnsubscribe(list, subscription, campaignId, ip, res, next) {
|
function handleUnsubscribe(list, subscription, autoUnsubscribe, campaignId, ip, res, next) {
|
||||||
if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP ||
|
if ((list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
|
||||||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
(autoUnsubscribe && (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
|
||||||
|
|
||||||
|
subscriptions.changeStatus(list.id, subscription.id, campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Shall we do anything with "found"?
|
||||||
|
|
||||||
|
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
|
@ -799,24 +822,6 @@ function handleUnsubscribe(list, subscription, campaignId, ip, res, next) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP ||
|
|
||||||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) {
|
|
||||||
|
|
||||||
subscriptions.changeStatus(list.id, subscription.id, campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Shall we do anything with "found"?
|
|
||||||
|
|
||||||
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else { // UnsubscriptionMode.MANUAL
|
} else { // UnsubscriptionMode.MANUAL
|
||||||
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice');
|
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice');
|
||||||
}
|
}
|
||||||
|
|
|
@ -466,12 +466,20 @@ let sendLoop = () => {
|
||||||
return setTimeout(sendLoop, 10 * 1000);
|
return setTimeout(sendLoop, 10 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isThrottled = false;
|
||||||
|
|
||||||
let getNext = () => {
|
let getNext = () => {
|
||||||
if (!mailer.transport.isIdle()) {
|
if (!mailer.transport.isIdle() || isThrottled) {
|
||||||
// only retrieve new messages if there are free slots in the mailer queue
|
// only retrieve new messages if there are free slots in the mailer queue
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isThrottled = true;
|
||||||
|
|
||||||
|
mailer.transport.checkThrottling(() => {
|
||||||
|
|
||||||
|
isThrottled = false;
|
||||||
|
|
||||||
// find an unsent message
|
// find an unsent message
|
||||||
findUnsent((err, message) => {
|
findUnsent((err, message) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -484,7 +492,7 @@ let sendLoop = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.verbose('Mail', 'Found new message to be delivered: %s', message.subscription.cid);
|
// log.verbose('Mail', 'Found new message to be delivered: %s', message.subscription.cid);
|
||||||
// format message to nodemailer message format
|
// format message to nodemailer message format
|
||||||
formatMessage(message, (err, mail) => {
|
formatMessage(message, (err, mail) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -499,6 +507,7 @@ let sendLoop = () => {
|
||||||
setTimeout(getNext, mailing_timeout);
|
setTimeout(getNext, mailing_timeout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!blacklisted) {
|
if (!blacklisted) {
|
||||||
let tryCount = 0;
|
let tryCount = 0;
|
||||||
let trySend = () => {
|
let trySend = () => {
|
||||||
|
@ -571,14 +580,15 @@ let sendLoop = () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setImmediate(() => mailer.transport.checkThrottling(getNext));
|
setImmediate(getNext);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
mailer.transport.on('idle', () => mailer.transport.checkThrottling(getNext));
|
mailer.transport.on('idle', getNext);
|
||||||
setImmediate(() => mailer.transport.checkThrottling(getNext));
|
setImmediate(getNext);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ set -e
|
||||||
|
|
||||||
yum -y install epel-release
|
yum -y install epel-release
|
||||||
|
|
||||||
curl --silent --location https://rpm.nodesource.com/setup_6.x | bash -
|
curl --silent --location https://rpm.nodesource.com/setup_7.x | bash -
|
||||||
yum -y install mariadb-server nodejs ImageMagick git python redis pwgen bind-utils gcc-c++ make
|
yum -y install mariadb-server nodejs ImageMagick git python redis pwgen bind-utils gcc-c++ make
|
||||||
|
|
||||||
systemctl start mariadb
|
systemctl start mariadb
|
||||||
|
@ -225,4 +225,3 @@ systemctl start zone-mta.service
|
||||||
systemctl start mailtrain.service
|
systemctl start mailtrain.service
|
||||||
|
|
||||||
echo "Success! Open http://$HOSTNAME/ and log in as admin:test";
|
echo "Success! Open http://$HOSTNAME/ and log in as admin:test";
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ set -e
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
curl -sL https://deb.nodesource.com/setup_6.x | bash -
|
curl -sL https://deb.nodesource.com/setup_7.x | bash -
|
||||||
apt-get -q -y install mariadb-server pwgen nodejs imagemagick git ufw build-essential dnsutils python software-properties-common
|
apt-get -q -y install mariadb-server pwgen nodejs imagemagick git ufw build-essential dnsutils python software-properties-common
|
||||||
|
|
||||||
apt-add-repository -y ppa:chris-lea/redis-server
|
apt-add-repository -y ppa:chris-lea/redis-server
|
||||||
|
|
|
@ -69,7 +69,6 @@ CREATE TABLE `campaigns` (
|
||||||
`html_prepared` longtext,
|
`html_prepared` longtext,
|
||||||
`text` longtext,
|
`text` longtext,
|
||||||
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
`tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
|
||||||
`scheduled` timestamp NULL DEFAULT NULL,
|
`scheduled` timestamp NULL DEFAULT NULL,
|
||||||
`status_change` timestamp NULL DEFAULT NULL,
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
`delivered` int(11) unsigned NOT NULL DEFAULT '0',
|
`delivered` int(11) unsigned NOT NULL DEFAULT '0',
|
||||||
|
@ -80,6 +79,8 @@ CREATE TABLE `campaigns` (
|
||||||
`bounced` int(1) unsigned NOT NULL DEFAULT '0',
|
`bounced` int(1) unsigned NOT NULL DEFAULT '0',
|
||||||
`complained` int(1) unsigned NOT NULL DEFAULT '0',
|
`complained` int(1) unsigned NOT NULL DEFAULT '0',
|
||||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`open_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
|
`click_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `cid` (`cid`),
|
UNIQUE KEY `cid` (`cid`),
|
||||||
KEY `name` (`name`(191)),
|
KEY `name` (`name`(191)),
|
||||||
|
@ -93,8 +94,8 @@ CREATE TABLE `confirmations` (
|
||||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
`list` int(11) unsigned NOT NULL,
|
`list` int(11) unsigned NOT NULL,
|
||||||
`email` varchar(255) NOT NULL,
|
`action` varchar(100) NOT NULL,
|
||||||
`opt_in_ip` varchar(100) DEFAULT NULL,
|
`ip` varchar(100) DEFAULT NULL,
|
||||||
`data` text NOT NULL,
|
`data` text NOT NULL,
|
||||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
@ -193,11 +194,17 @@ CREATE TABLE `lists` (
|
||||||
`subscribers` int(11) unsigned DEFAULT '0',
|
`subscribers` int(11) unsigned DEFAULT '0',
|
||||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`public_subscribe` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
`public_subscribe` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`unsubscription_mode` int(11) unsigned NOT NULL DEFAULT '0',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `cid` (`cid`),
|
UNIQUE KEY `cid` (`cid`),
|
||||||
KEY `name` (`name`(191))
|
KEY `name` (`name`(191))
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;
|
||||||
INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`) VALUES (1,'Hkj1vCoJb',NULL,'01 Testlist - Public Subscribe','',0,NOW(),1);
|
INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (1,'Hkj1vCoJb',0,'#1 (one-step, no form)','',0,NOW(),1,0);
|
||||||
|
INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (2,'SktV4HDZ-',NULL,'#2 (one-step, with form)','',0,NOW(),1,1);
|
||||||
|
INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (3,'BkdvNBw-W',NULL,'#3 (two-step, no form)','',0,NOW(),1,2);
|
||||||
|
INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (4,'rJMKVrDZ-',NULL,'#4 (two-step, with form)','',0,NOW(),1,3);
|
||||||
|
INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (5,'SJgoNSw-W',NULL,'#5 (manual unsubscribe)','',0,NOW(),1,4);
|
||||||
|
INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (6,'HyveEPvWW',NULL,'#6 (non-public)','',0,NOW(),0,0);
|
||||||
CREATE TABLE `queued` (
|
CREATE TABLE `queued` (
|
||||||
`campaign` int(11) unsigned NOT NULL,
|
`campaign` int(11) unsigned NOT NULL,
|
||||||
`list` int(11) unsigned NOT NULL,
|
`list` int(11) unsigned NOT NULL,
|
||||||
|
@ -269,7 +276,7 @@ CREATE TABLE `settings` (
|
||||||
`value` text NOT NULL,
|
`value` text NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `key` (`key`)
|
UNIQUE KEY `key` (`key`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=112 DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8mb4;
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','5587');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','5587');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','NONE');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','NONE');
|
||||||
|
@ -286,7 +293,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','https://mailtrain.org');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','https://mailtrain.org');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','27');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','29');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (46,'ua_code','');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (46,'ua_code','');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (47,'shoutout','');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (47,'shoutout','');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (54,'mail_transport','smtp');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (54,'mail_transport','smtp');
|
||||||
|
@ -361,6 +368,146 @@ CREATE TABLE `subscription__1` (
|
||||||
KEY `latest_click` (`latest_click`),
|
KEY `latest_click` (`latest_click`),
|
||||||
KEY `created` (`created`)
|
KEY `created` (`created`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
CREATE TABLE `subscription__2` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
|
`email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||||
|
`opt_in_ip` varchar(100) DEFAULT NULL,
|
||||||
|
`opt_in_country` varchar(2) DEFAULT NULL,
|
||||||
|
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||||
|
`imported` int(11) unsigned DEFAULT NULL,
|
||||||
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_open` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_click` timestamp NULL DEFAULT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`first_name` varchar(255) DEFAULT NULL,
|
||||||
|
`last_name` varchar(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `email` (`email`),
|
||||||
|
UNIQUE KEY `cid` (`cid`),
|
||||||
|
KEY `status` (`status`),
|
||||||
|
KEY `first_name` (`first_name`(191)),
|
||||||
|
KEY `last_name` (`last_name`(191)),
|
||||||
|
KEY `subscriber_tz` (`tz`),
|
||||||
|
KEY `is_test` (`is_test`),
|
||||||
|
KEY `latest_open` (`latest_open`),
|
||||||
|
KEY `latest_click` (`latest_click`),
|
||||||
|
KEY `created` (`created`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
CREATE TABLE `subscription__3` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
|
`email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||||
|
`opt_in_ip` varchar(100) DEFAULT NULL,
|
||||||
|
`opt_in_country` varchar(2) DEFAULT NULL,
|
||||||
|
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||||
|
`imported` int(11) unsigned DEFAULT NULL,
|
||||||
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_open` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_click` timestamp NULL DEFAULT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`first_name` varchar(255) DEFAULT NULL,
|
||||||
|
`last_name` varchar(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `email` (`email`),
|
||||||
|
UNIQUE KEY `cid` (`cid`),
|
||||||
|
KEY `status` (`status`),
|
||||||
|
KEY `first_name` (`first_name`(191)),
|
||||||
|
KEY `last_name` (`last_name`(191)),
|
||||||
|
KEY `subscriber_tz` (`tz`),
|
||||||
|
KEY `is_test` (`is_test`),
|
||||||
|
KEY `latest_open` (`latest_open`),
|
||||||
|
KEY `latest_click` (`latest_click`),
|
||||||
|
KEY `created` (`created`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
CREATE TABLE `subscription__4` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
|
`email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||||
|
`opt_in_ip` varchar(100) DEFAULT NULL,
|
||||||
|
`opt_in_country` varchar(2) DEFAULT NULL,
|
||||||
|
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||||
|
`imported` int(11) unsigned DEFAULT NULL,
|
||||||
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_open` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_click` timestamp NULL DEFAULT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`first_name` varchar(255) DEFAULT NULL,
|
||||||
|
`last_name` varchar(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `email` (`email`),
|
||||||
|
UNIQUE KEY `cid` (`cid`),
|
||||||
|
KEY `status` (`status`),
|
||||||
|
KEY `first_name` (`first_name`(191)),
|
||||||
|
KEY `last_name` (`last_name`(191)),
|
||||||
|
KEY `subscriber_tz` (`tz`),
|
||||||
|
KEY `is_test` (`is_test`),
|
||||||
|
KEY `latest_open` (`latest_open`),
|
||||||
|
KEY `latest_click` (`latest_click`),
|
||||||
|
KEY `created` (`created`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
CREATE TABLE `subscription__5` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
|
`email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||||
|
`opt_in_ip` varchar(100) DEFAULT NULL,
|
||||||
|
`opt_in_country` varchar(2) DEFAULT NULL,
|
||||||
|
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||||
|
`imported` int(11) unsigned DEFAULT NULL,
|
||||||
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_open` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_click` timestamp NULL DEFAULT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`first_name` varchar(255) DEFAULT NULL,
|
||||||
|
`last_name` varchar(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `email` (`email`),
|
||||||
|
UNIQUE KEY `cid` (`cid`),
|
||||||
|
KEY `status` (`status`),
|
||||||
|
KEY `first_name` (`first_name`(191)),
|
||||||
|
KEY `last_name` (`last_name`(191)),
|
||||||
|
KEY `subscriber_tz` (`tz`),
|
||||||
|
KEY `is_test` (`is_test`),
|
||||||
|
KEY `latest_open` (`latest_open`),
|
||||||
|
KEY `latest_click` (`latest_click`),
|
||||||
|
KEY `created` (`created`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
CREATE TABLE `subscription__6` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
|
`email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||||
|
`opt_in_ip` varchar(100) DEFAULT NULL,
|
||||||
|
`opt_in_country` varchar(2) DEFAULT NULL,
|
||||||
|
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||||
|
`imported` int(11) unsigned DEFAULT NULL,
|
||||||
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_open` timestamp NULL DEFAULT NULL,
|
||||||
|
`latest_click` timestamp NULL DEFAULT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`first_name` varchar(255) DEFAULT NULL,
|
||||||
|
`last_name` varchar(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `email` (`email`),
|
||||||
|
UNIQUE KEY `cid` (`cid`),
|
||||||
|
KEY `status` (`status`),
|
||||||
|
KEY `first_name` (`first_name`(191)),
|
||||||
|
KEY `last_name` (`last_name`(191)),
|
||||||
|
KEY `subscriber_tz` (`tz`),
|
||||||
|
KEY `is_test` (`is_test`),
|
||||||
|
KEY `latest_open` (`latest_open`),
|
||||||
|
KEY `latest_click` (`latest_click`),
|
||||||
|
KEY `created` (`created`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
CREATE TABLE `templates` (
|
CREATE TABLE `templates` (
|
||||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`name` varchar(255) NOT NULL DEFAULT '',
|
`name` varchar(255) NOT NULL DEFAULT '',
|
||||||
|
@ -422,14 +569,14 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/blantyre',120);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/brazzaville',60);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/brazzaville',60);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bujumbura',120);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bujumbura',120);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/cairo',120);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/cairo',120);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',60);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',0);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ceuta',120);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ceuta',120);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/conakry',0);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/conakry',0);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dakar',0);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dakar',0);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dar_es_salaam',180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dar_es_salaam',180);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/djibouti',180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/djibouti',180);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/douala',60);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/douala',60);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',60);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',0);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/freetown',0);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/freetown',0);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/gaborone',120);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/gaborone',120);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/harare',120);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/harare',120);
|
||||||
|
@ -603,7 +750,7 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rio_branco',-300);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rosario',-180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rosario',-180);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santarem',-180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santarem',-180);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santa_isabel',-420);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santa_isabel',-420);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-240);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santo_domingo',-240);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santo_domingo',-240);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sao_paulo',-180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sao_paulo',-180);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/scoresbysund',0);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/scoresbysund',0);
|
||||||
|
@ -788,8 +935,8 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/pacific',-420);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/saskatchewan',-360);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/saskatchewan',-360);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/yukon',-420);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/yukon',-420);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cet',120);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cet',120);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-240);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-300);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-360);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cst6cdt',-300);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cst6cdt',-300);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cuba',-240);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cuba',-240);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eet',180);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eet',180);
|
||||||
|
@ -936,7 +1083,7 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/auckland',720);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/bougainville',660);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/bougainville',660);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chatham',765);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chatham',765);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chuuk',600);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chuuk',600);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-300);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-360);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/efate',660);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/efate',660);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/enderbury',780);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/enderbury',780);
|
||||||
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fakaofo',780);
|
INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fakaofo',780);
|
||||||
|
|
13
setup/sql/upgrade-00029.sql
Normal file
13
setup/sql/upgrade-00029.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Header section
|
||||||
|
# Define incrementing schema version number
|
||||||
|
SET @schema_version = '29';
|
||||||
|
|
||||||
|
# Rename column tracking_disabled
|
||||||
|
ALTER TABLE `campaigns` ADD COLUMN `open_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL, ADD COLUMN `click_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL;
|
||||||
|
UPDATE `campaigns` SET `open_tracking_disabled` = `tracking_disabled`, `click_tracking_disabled` = `tracking_disabled`;
|
||||||
|
ALTER TABLE `campaigns` DROP COLUMN `tracking_disabled`;
|
||||||
|
|
||||||
|
# Footer section
|
||||||
|
LOCK TABLES `settings` WRITE;
|
||||||
|
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
|
||||||
|
UNLOCK TABLES;
|
|
@ -2,8 +2,9 @@
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
"rules": {
|
"rules": {
|
||||||
"strict": 0,
|
"strict": 0,
|
||||||
"no-invalid-this": 0,
|
"no-console": 0,
|
||||||
"no-unused-expressions": 0
|
"comma-dangle": 0,
|
||||||
|
"arrow-body-style": 0
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"mocha": true
|
"mocha": true
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const config = require('config');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
app: config,
|
|
||||||
baseUrl: 'http://localhost:' + config.www.port,
|
|
||||||
users: {
|
|
||||||
admin: {
|
|
||||||
username: 'admin',
|
|
||||||
password: 'test'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lists: {
|
|
||||||
one: {
|
|
||||||
id: 1,
|
|
||||||
cid: 'Hkj1vCoJb',
|
|
||||||
publicSubscribe: 1,
|
|
||||||
unsubscriptionMode: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
'service-url' : 'http://localhost:' + config.www.port + '/',
|
|
||||||
'default-homepage': 'https://mailtrain.org',
|
|
||||||
'smtp-hostname': config.testserver.host,
|
|
||||||
'smtp-port': config.testserver.port,
|
|
||||||
'smtp-encryption': 'NONE',
|
|
||||||
'smtp-user': config.testserver.username,
|
|
||||||
'smtp-pass': config.testserver.password
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const config = require('./config');
|
|
||||||
const webdriver = require('selenium-webdriver');
|
|
||||||
|
|
||||||
const driver = new webdriver.Builder()
|
|
||||||
.forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
|
|
||||||
.build();
|
|
||||||
|
|
||||||
if (global.USE_SHARED_DRIVER === true) {
|
|
||||||
driver.originalQuit = driver.quit;
|
|
||||||
driver.quit = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = driver;
|
|
|
@ -1,36 +1,28 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
require('./helpers/exit-unless-test');
|
require('./lib/exit-unless-test');
|
||||||
|
const mocha = require('./lib/mocha-e2e').mocha;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
global.USE_SHARED_DRIVER = true;
|
|
||||||
|
|
||||||
const driver = require('./helpers/driver');
|
|
||||||
const only = 'only';
|
const only = 'only';
|
||||||
const skip = 'skip';
|
const skip = 'skip';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let tests = [
|
let tests = [
|
||||||
['tests/login'],
|
'login',
|
||||||
['tests/subscription']
|
'subscription'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
tests = tests.map(testSpec => (testSpec.constructor === Array ? testSpec : [testSpec]));
|
||||||
|
tests = tests.filter(testSpec => testSpec[1] !== skip);
|
||||||
tests = tests.filter(t => t[1] !== skip);
|
if (tests.some(testSpec => testSpec[1] === only)) {
|
||||||
|
tests = tests.filter(testSpec => testSpec[1] === only);
|
||||||
if (tests.some(t => t[1] === only)) {
|
|
||||||
tests = tests.filter(t => t[1] === only);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('e2e', function() {
|
for (const testSpec of tests) {
|
||||||
this.timeout(10000);
|
const testPath = path.join(__dirname, 'tests', testSpec[0] + '.js');
|
||||||
|
mocha.addFile(testPath);
|
||||||
|
}
|
||||||
|
|
||||||
tests.forEach(t => {
|
mocha.run(failures => {
|
||||||
describe(t[0], () => {
|
process.exit(failures); // exit with non-zero status if there were failures
|
||||||
require('./' + t[0]); // eslint-disable-line global-require
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => driver.originalQuit());
|
|
||||||
});
|
});
|
||||||
|
|
63
test/e2e/lib/config.js
Normal file
63
test/e2e/lib/config.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('config');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
app: config,
|
||||||
|
baseUrl: 'http://localhost:' + config.www.port,
|
||||||
|
mailUrl: 'http://localhost:' + config.testserver.mailboxserverport,
|
||||||
|
users: {
|
||||||
|
admin: {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'test'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lists: {
|
||||||
|
l1: {
|
||||||
|
id: 1,
|
||||||
|
cid: 'Hkj1vCoJb',
|
||||||
|
publicSubscribe: 1,
|
||||||
|
unsubscriptionMode: 0, // (one-step, no form)
|
||||||
|
},
|
||||||
|
l2: {
|
||||||
|
id: 2,
|
||||||
|
cid: 'SktV4HDZ-',
|
||||||
|
publicSubscribe: 1,
|
||||||
|
unsubscriptionMode: 1, // (one-step, with form)
|
||||||
|
},
|
||||||
|
l3: {
|
||||||
|
id: 3,
|
||||||
|
cid: 'BkdvNBw-W',
|
||||||
|
publicSubscribe: 1,
|
||||||
|
unsubscriptionMode: 2, // (two-step, no form)
|
||||||
|
},
|
||||||
|
l4: {
|
||||||
|
id: 4,
|
||||||
|
cid: 'rJMKVrDZ-',
|
||||||
|
publicSubscribe: 1,
|
||||||
|
unsubscriptionMode: 3, // (two-step, with form)
|
||||||
|
},
|
||||||
|
l5: {
|
||||||
|
id: 5,
|
||||||
|
cid: 'SJgoNSw-W',
|
||||||
|
publicSubscribe: 1,
|
||||||
|
unsubscriptionMode: 4, // (manual unsubscribe)
|
||||||
|
},
|
||||||
|
l6: {
|
||||||
|
id: 6,
|
||||||
|
cid: 'HyveEPvWW',
|
||||||
|
publicSubscribe: 0,
|
||||||
|
unsubscriptionMode: 0, // (one-step, no form)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'service-url': 'http://localhost:' + config.www.port + '/',
|
||||||
|
'admin-email': 'admin@example.com',
|
||||||
|
'default-homepage': 'https://mailtrain.org',
|
||||||
|
'smtp-hostname': config.testserver.host,
|
||||||
|
'smtp-port': config.testserver.port,
|
||||||
|
'smtp-encryption': 'NONE',
|
||||||
|
'smtp-user': config.testserver.username,
|
||||||
|
'smtp-pass': config.testserver.password
|
||||||
|
}
|
||||||
|
};
|
19
test/e2e/lib/mail.js
Normal file
19
test/e2e/lib/mail.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('./config');
|
||||||
|
const driver = require('./mocha-e2e').driver;
|
||||||
|
const page = require('./page');
|
||||||
|
|
||||||
|
module.exports = (...extras) => page({
|
||||||
|
|
||||||
|
async fetchMail(address) {
|
||||||
|
await driver.sleep(1000);
|
||||||
|
await driver.navigate().to(`${config.mailUrl}/${address}`);
|
||||||
|
await this.waitUntilVisible();
|
||||||
|
},
|
||||||
|
|
||||||
|
async ensureUrl() {
|
||||||
|
throw new Error('Unsupported method.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}, ...extras);
|
213
test/e2e/lib/mocha-e2e.js
Normal file
213
test/e2e/lib/mocha-e2e.js
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
const Mocha = require('mocha');
|
||||||
|
const color = Mocha.reporters.Base.color;
|
||||||
|
const WorkerCounter = require('./worker-counter');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const config = require('./config');
|
||||||
|
const webdriver = require('selenium-webdriver');
|
||||||
|
|
||||||
|
const driver = new webdriver.Builder()
|
||||||
|
.forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const failHandlerRunning = new WorkerCounter();
|
||||||
|
|
||||||
|
function UseCaseReporter(runner) {
|
||||||
|
Mocha.reporters.Base.call(this, runner);
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
let indents = 0;
|
||||||
|
|
||||||
|
function indent () {
|
||||||
|
return Array(indents).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
runner.on('start', () => {
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('suite', suite => {
|
||||||
|
++indents;
|
||||||
|
console.log(color('suite', '%s%s'), indent(), suite.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('suite end', () => {
|
||||||
|
--indents;
|
||||||
|
if (indents === 1) {
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('use-case', useCase => {
|
||||||
|
++indents;
|
||||||
|
console.log();
|
||||||
|
console.log(color('suite', '%sUse case: %s'), indent(), useCase.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('use-case end', () => {
|
||||||
|
--indents;
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('steps', useCase => {
|
||||||
|
++indents;
|
||||||
|
console.log(color('pass', '%s%s'), indent(), useCase.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('steps end', () => {
|
||||||
|
--indents;
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('step pass', step => {
|
||||||
|
console.log(indent() + color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + color('pass', ' %s'), step.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('step fail', step => {
|
||||||
|
console.log(indent() + color('fail', ' %s'), step.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('pending', test => {
|
||||||
|
const fmt = indent() + color('pending', ' - %s');
|
||||||
|
console.log(fmt, test.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('pass', test => {
|
||||||
|
let fmt;
|
||||||
|
if (test.speed === 'fast') {
|
||||||
|
fmt = indent() +
|
||||||
|
color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) +
|
||||||
|
color('pass', ' %s');
|
||||||
|
console.log(fmt, test.title);
|
||||||
|
} else {
|
||||||
|
fmt = indent() +
|
||||||
|
color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) +
|
||||||
|
color('pass', ' %s') +
|
||||||
|
color(test.speed, ' (%dms)');
|
||||||
|
console.log(fmt, test.title, test.duration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('fail', (test, err) => {
|
||||||
|
failHandlerRunning.enter();
|
||||||
|
(async () => {
|
||||||
|
const currentUrl = await driver.getCurrentUrl();
|
||||||
|
const info = `URL: ${currentUrl}`;
|
||||||
|
await fs.writeFile('last-failed-e2e-test.info', info);
|
||||||
|
await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource());
|
||||||
|
await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64'));
|
||||||
|
failHandlerRunning.exit();
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log(indent() + color('fail', ' %s'), test.title);
|
||||||
|
console.log();
|
||||||
|
console.log(err);
|
||||||
|
console.log();
|
||||||
|
console.log('Snaphot of and info about the current page are in last-failed-e2e-test.*');
|
||||||
|
});
|
||||||
|
|
||||||
|
runner.on('end', () => {
|
||||||
|
const stats = self.stats;
|
||||||
|
let fmt;
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// passes
|
||||||
|
fmt = color('bright pass', ' ') + color('green', ' %d passing');
|
||||||
|
console.log(fmt, stats.passes);
|
||||||
|
|
||||||
|
// pending
|
||||||
|
if (stats.pending) {
|
||||||
|
fmt = color('pending', ' ') + color('pending', ' %d pending');
|
||||||
|
console.log(fmt, stats.pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
// failures
|
||||||
|
if (stats.failures) {
|
||||||
|
fmt = color('fail', ' %d failing');
|
||||||
|
console.log(fmt, stats.failures);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const mocha = new Mocha()
|
||||||
|
.timeout(120000)
|
||||||
|
.reporter(UseCaseReporter)
|
||||||
|
.ui('tdd');
|
||||||
|
|
||||||
|
mocha._originalRun = mocha.run;
|
||||||
|
|
||||||
|
|
||||||
|
let runner;
|
||||||
|
mocha.run = fn => {
|
||||||
|
runner = mocha._originalRun(async () => {
|
||||||
|
await failHandlerRunning.waitForEmpty();
|
||||||
|
await driver.quit();
|
||||||
|
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
async function useCaseExec(name, asyncFn) {
|
||||||
|
runner.emit('use-case', {title: name});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await asyncFn();
|
||||||
|
runner.emit('use-case end');
|
||||||
|
} catch (err) {
|
||||||
|
runner.emit('use-case end');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCase(name, asyncFn) {
|
||||||
|
if (asyncFn) {
|
||||||
|
return test('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||||
|
} else {
|
||||||
|
// Pending test
|
||||||
|
return test('Use case: ' + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useCase.only = (name, asyncFn) => test.only('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||||
|
|
||||||
|
useCase.skip = (name, asyncFn) => test.skip('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||||
|
|
||||||
|
async function step(name, asyncFn) {
|
||||||
|
try {
|
||||||
|
await asyncFn();
|
||||||
|
runner.emit('step pass', {title: name});
|
||||||
|
} catch (err) {
|
||||||
|
runner.emit('step fail', {title: name});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function steps(name, asyncFn) {
|
||||||
|
try {
|
||||||
|
runner.emit('steps', {title: name});
|
||||||
|
await asyncFn();
|
||||||
|
runner.emit('steps end');
|
||||||
|
} catch (err) {
|
||||||
|
runner.emit('step end');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function precondition(preConditionName, useCaseName, asyncFn) {
|
||||||
|
await steps(`Including use case "${useCaseName}" to satisfy precondition "${preConditionName}"`, asyncFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mocha,
|
||||||
|
useCase,
|
||||||
|
step,
|
||||||
|
steps,
|
||||||
|
precondition,
|
||||||
|
driver
|
||||||
|
};
|
123
test/e2e/lib/page.js
Normal file
123
test/e2e/lib/page.js
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const webdriver = require('selenium-webdriver');
|
||||||
|
const By = webdriver.By;
|
||||||
|
const until = webdriver.until;
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const driver = require('./mocha-e2e').driver;
|
||||||
|
const url = require('url');
|
||||||
|
const UrlPattern = require('url-pattern');
|
||||||
|
|
||||||
|
const waitTimeout = 10000;
|
||||||
|
|
||||||
|
module.exports = (...extras) => Object.assign({
|
||||||
|
elements: {},
|
||||||
|
|
||||||
|
async getElement(key) {
|
||||||
|
return await driver.findElement(By.css(this.elements[key]));
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLinkParams(key) {
|
||||||
|
const elem = await driver.findElement(By.css(this.elements[key]));
|
||||||
|
|
||||||
|
const linkUrl = await elem.getAttribute('href');
|
||||||
|
const linkPath = url.parse(linkUrl).path;
|
||||||
|
|
||||||
|
const urlPattern = new UrlPattern(this.links[key]);
|
||||||
|
|
||||||
|
const params = urlPattern.match(linkPath);
|
||||||
|
if (!params) {
|
||||||
|
throw new Error(`Cannot match URL pattern ${this.links[key]}`);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
|
||||||
|
async waitUntilVisible(selector) {
|
||||||
|
await driver.wait(until.elementLocated(By.css('body')), waitTimeout);
|
||||||
|
|
||||||
|
if (selector) {
|
||||||
|
await driver.wait(until.elementLocated(By.css(selector)), waitTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const elem of (this.elementsToWaitFor || [])) {
|
||||||
|
const sel = this.elements[elem];
|
||||||
|
if (!sel) {
|
||||||
|
throw new Error(`Element "${elem}" not found.`);
|
||||||
|
}
|
||||||
|
await driver.wait(until.elementLocated(By.css(sel)), waitTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const text of (this.textsToWaitFor || [])) {
|
||||||
|
await driver.wait(new webdriver.Condition(`for text "${text}"`, async () => await this.containsText(text)), waitTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.url) {
|
||||||
|
await this.ensureUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
await driver.executeScript('document.mailTrainRefreshAcknowledged = true;');
|
||||||
|
},
|
||||||
|
|
||||||
|
async waitUntilVisibleAfterRefresh(selector) {
|
||||||
|
await driver.wait(new webdriver.Condition('for refresh', async driver => {
|
||||||
|
const val = await driver.executeScript('return document.mailTrainRefreshAcknowledged;');
|
||||||
|
return !val;
|
||||||
|
}), waitTimeout);
|
||||||
|
|
||||||
|
await this.waitUntilVisible(selector);
|
||||||
|
},
|
||||||
|
|
||||||
|
async click(key) {
|
||||||
|
const elem = await this.getElement(key);
|
||||||
|
await elem.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
async getHref(key) {
|
||||||
|
const elem = await this.getElement(key);
|
||||||
|
return await elem.getAttribute('href');
|
||||||
|
},
|
||||||
|
|
||||||
|
async getText(key) {
|
||||||
|
const elem = await this.getElement(key);
|
||||||
|
return await elem.getText();
|
||||||
|
},
|
||||||
|
|
||||||
|
async getValue(key) {
|
||||||
|
const elem = await this.getElement(key);
|
||||||
|
return await elem.getAttribute('value');
|
||||||
|
},
|
||||||
|
|
||||||
|
async containsText(str) {
|
||||||
|
return await driver.executeScript(`
|
||||||
|
return (document.documentElement.innerText || document.documentElement.textContent).indexOf('${str}') > -1;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSource() {
|
||||||
|
return await driver.getPageSource();
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSource(destPath) {
|
||||||
|
const src = await this.getSource();
|
||||||
|
await fs.writeFile(destPath, src);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveScreenshot(destPath) {
|
||||||
|
const pngData = await driver.takeScreenshot();
|
||||||
|
const buf = new Buffer(pngData, 'base64');
|
||||||
|
await fs.writeFile(destPath, buf);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSnapshot(destPathBase) {
|
||||||
|
destPathBase = destPathBase || 'last-failed-e2e-test';
|
||||||
|
const currentUrl = await driver.getCurrentUrl();
|
||||||
|
const info = `URL: ${currentUrl}`;
|
||||||
|
await fs.writeFile(destPathBase + '.info', info);
|
||||||
|
await this.saveSource(destPathBase + '.html');
|
||||||
|
await this.saveScreenshot(destPathBase + '.png');
|
||||||
|
},
|
||||||
|
|
||||||
|
async sleep(ms) {
|
||||||
|
await driver.sleep(ms);
|
||||||
|
}
|
||||||
|
}, ...extras);
|
77
test/e2e/lib/web.js
Normal file
77
test/e2e/lib/web.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('./config');
|
||||||
|
const By = require('selenium-webdriver').By;
|
||||||
|
const url = require('url');
|
||||||
|
const UrlPattern = require('url-pattern');
|
||||||
|
const driver = require('./mocha-e2e').driver;
|
||||||
|
const page = require('./page');
|
||||||
|
|
||||||
|
module.exports = (...extras) => page({
|
||||||
|
|
||||||
|
async navigate(pathOrParams) {
|
||||||
|
let path;
|
||||||
|
if (typeof pathOrParams === 'string') {
|
||||||
|
path = pathOrParams;
|
||||||
|
} else {
|
||||||
|
const urlPattern = new UrlPattern(this.requestUrl || this.url);
|
||||||
|
path = urlPattern.stringify(pathOrParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedUrl = url.parse(path);
|
||||||
|
let absolutePath;
|
||||||
|
if (parsedUrl.host) {
|
||||||
|
absolutePath = path;
|
||||||
|
} else {
|
||||||
|
absolutePath = config.baseUrl + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
await driver.navigate().to(absolutePath);
|
||||||
|
await this.waitUntilVisible();
|
||||||
|
},
|
||||||
|
|
||||||
|
async ensureUrl(path) {
|
||||||
|
const desiredUrl = path || this.url;
|
||||||
|
|
||||||
|
if (desiredUrl) {
|
||||||
|
const currentUrl = url.parse(await driver.getCurrentUrl());
|
||||||
|
const urlPattern = new UrlPattern(desiredUrl);
|
||||||
|
const params = urlPattern.match(currentUrl.pathname);
|
||||||
|
if (!params || config.baseUrl !== `${currentUrl.protocol}//${currentUrl.host}`) {
|
||||||
|
throw new Error(`Unexpected URL. Expecting ${config.baseUrl}${this.url} got ${currentUrl.protocol}//${currentUrl.host}/${currentUrl.pathname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.params = params;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const submitButton = await this.getElement('submitButton');
|
||||||
|
await submitButton.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
async waitForFlash() {
|
||||||
|
await this.waitUntilVisible('div.alert:not(.js-warning)');
|
||||||
|
},
|
||||||
|
|
||||||
|
async getFlash() {
|
||||||
|
const elem = await driver.findElement(By.css('div.alert:not(.js-warning)'));
|
||||||
|
return await elem.getText();
|
||||||
|
},
|
||||||
|
|
||||||
|
async clearFlash() {
|
||||||
|
await driver.executeScript(`
|
||||||
|
var elements = document.getElementsByClassName('alert');
|
||||||
|
while(elements.length > 0){
|
||||||
|
elements[0].parentNode.removeChild(elements[0]);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async setValue(key, value) {
|
||||||
|
const elem = await this.getElement(key);
|
||||||
|
await elem.clear();
|
||||||
|
await elem.sendKeys(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, ...extras);
|
35
test/e2e/lib/worker-counter.js
Normal file
35
test/e2e/lib/worker-counter.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
|
class WorkerCounter {
|
||||||
|
constructor() {
|
||||||
|
this.counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
enter() {
|
||||||
|
this.counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit() {
|
||||||
|
this.counter--;
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForEmpty() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
function wait(resolve) {
|
||||||
|
if (self.counter === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
setTimeout(wait, 500, resolve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(wait, 500, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WorkerCounter;
|
|
@ -1,21 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const page = require('./page');
|
|
||||||
|
|
||||||
module.exports = driver => Object.assign(page(driver), {
|
|
||||||
elementToWaitFor: 'alert',
|
|
||||||
elements: {
|
|
||||||
alert: 'div.alert:not(.js-warning)'
|
|
||||||
},
|
|
||||||
getText() {
|
|
||||||
return this.element('alert').getText();
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
return this.driver.executeScript(`
|
|
||||||
var elements = document.getElementsByClassName('alert');
|
|
||||||
while(elements.length > 0){
|
|
||||||
elements[0].parentNode.removeChild(elements[0]);
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,11 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const page = require('./page');
|
const web = require('../lib/web');
|
||||||
|
|
||||||
module.exports = driver => Object.assign(page(driver), {
|
module.exports = web({
|
||||||
url: '/',
|
url: '/'
|
||||||
elementToWaitFor: 'body',
|
|
||||||
elements: {
|
|
||||||
body: 'body.page--home'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const config = require('../helpers/config');
|
|
||||||
const webdriver = require('selenium-webdriver');
|
|
||||||
const By = webdriver.By;
|
|
||||||
const until = webdriver.until;
|
|
||||||
|
|
||||||
module.exports = driver => ({
|
|
||||||
driver,
|
|
||||||
elements: {},
|
|
||||||
|
|
||||||
element(key) {
|
|
||||||
return this.driver.findElement(By.css(this.elements[key] || key));
|
|
||||||
},
|
|
||||||
|
|
||||||
navigate(path) {
|
|
||||||
this.driver.navigate().to(config.baseUrl + (path || this.url));
|
|
||||||
return this.waitUntilVisible();
|
|
||||||
},
|
|
||||||
|
|
||||||
waitUntilVisible() {
|
|
||||||
let selector = this.elements[this.elementToWaitFor];
|
|
||||||
if (!selector && this.url) {
|
|
||||||
selector = 'body.page--' + (this.url.substring(1).replace(/\//g, '--') || 'home');
|
|
||||||
}
|
|
||||||
return selector ? this.driver.wait(until.elementLocated(By.css(selector))) : this.driver.sleep(1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
submit() {
|
|
||||||
return this.element('submitButton').click();
|
|
||||||
},
|
|
||||||
|
|
||||||
click(key) {
|
|
||||||
return this.element(key).click();
|
|
||||||
},
|
|
||||||
|
|
||||||
getText(key) {
|
|
||||||
return this.element(key).getText();
|
|
||||||
},
|
|
||||||
|
|
||||||
getValue(key) {
|
|
||||||
return this.element(key).getAttribute('value');
|
|
||||||
},
|
|
||||||
|
|
||||||
setValue(key, value) {
|
|
||||||
return this.element(key).sendKeys(value);
|
|
||||||
},
|
|
||||||
|
|
||||||
containsText(str) {
|
|
||||||
// let text = await driver.findElement({ css: 'body' }).getText();
|
|
||||||
return this.driver.executeScript(`
|
|
||||||
return (document.documentElement.textContent || document.documentElement.innerText).indexOf('${str}') > -1;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,84 +1,156 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const config = require('../helpers/config');
|
const config = require('../lib/config');
|
||||||
const page = require('./page');
|
const web = require('../lib/web');
|
||||||
|
const mail = require('../lib/mail');
|
||||||
|
|
||||||
const web = {
|
module.exports = list => ({
|
||||||
enterEmail(value) {
|
|
||||||
this.element('emailInput').clear();
|
|
||||||
return this.element('emailInput').sendKeys(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mail = {
|
webSubscribe: web({
|
||||||
navigate(address) {
|
|
||||||
this.driver.sleep(100);
|
|
||||||
this.driver.navigate().to(`http://localhost:${config.app.testserver.mailboxserverport}/${address}`);
|
|
||||||
return this.waitUntilVisible();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = (driver, list) => ({
|
|
||||||
|
|
||||||
webSubscribe: Object.assign(page(driver), web, {
|
|
||||||
url: `/subscription/${list.cid}`,
|
url: `/subscription/${list.cid}`,
|
||||||
elementToWaitFor: 'form',
|
elementsToWaitFor: ['form'],
|
||||||
|
textsToWaitFor: ['Subscribe to list'],
|
||||||
elements: {
|
elements: {
|
||||||
form: `form[action="/subscription/${list.cid}/subscribe"]`,
|
form: `form[action="/subscription/${list.cid}/subscribe"]`,
|
||||||
emailInput: '#main-form input[name="email"]',
|
emailInput: '#main-form input[name="email"]',
|
||||||
|
firstNameInput: '#main-form input[name="first-name"]',
|
||||||
|
lastNameInput: '#main-form input[name="last-name"]',
|
||||||
submitButton: 'a[href="#submit"]'
|
submitButton: 'a[href="#submit"]'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
webConfirmSubscriptionNotice: Object.assign(page(driver), web, {
|
webSubscribeNonPublic: web({
|
||||||
url: `/subscription/${list.cid}/confirm-notice`,
|
url: `/subscription/${list.cid}`,
|
||||||
elementToWaitFor: 'homepageButton',
|
textsToWaitFor: ['The list does not allow public subscriptions'],
|
||||||
|
}),
|
||||||
|
|
||||||
|
webConfirmSubscriptionNotice: web({
|
||||||
|
url: `/subscription/${list.cid}/confirm-subscription-notice`,
|
||||||
|
textsToWaitFor: ['We need to confirm your email address']
|
||||||
|
}),
|
||||||
|
|
||||||
|
mailConfirmSubscription: mail({
|
||||||
|
elementsToWaitFor: ['confirmLink'],
|
||||||
|
textsToWaitFor: ['Please Confirm Subscription'],
|
||||||
elements: {
|
elements: {
|
||||||
homepageButton: `a[href="${config.settings['default-homepage']}"]`
|
confirmLink: `a[href^="${config.settings['service-url']}subscription/confirm/subscribe/"]`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
mailConfirmSubscription: Object.assign(page(driver), mail, {
|
mailAlreadySubscribed: mail({
|
||||||
elementToWaitFor: 'confirmLink',
|
elementsToWaitFor: ['unsubscribeLink'],
|
||||||
|
textsToWaitFor: ['Email address already registered'],
|
||||||
elements: {
|
elements: {
|
||||||
confirmLink: `a[href^="${config.settings['service-url']}subscription/subscribe/"]`
|
unsubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/unsubscribe/"]`,
|
||||||
|
manageLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/manage/"]`
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
unsubscribeLink: `/subscription/${list.cid}/unsubscribe/:ucid`,
|
||||||
|
manageLink: `/subscription/${list.cid}/manage/:ucid`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
webSubscribedNotice: Object.assign(page(driver), web, {
|
webSubscribedNotice: web({
|
||||||
elementToWaitFor: 'homepageButton',
|
url: `/subscription/${list.cid}/subscribed-notice`,
|
||||||
|
textsToWaitFor: ['Subscription Confirmed']
|
||||||
|
}),
|
||||||
|
|
||||||
|
mailSubscriptionConfirmed: mail({
|
||||||
|
elementsToWaitFor: ['unsubscribeLink'],
|
||||||
|
textsToWaitFor: ['Subscription Confirmed'],
|
||||||
elements: {
|
elements: {
|
||||||
homepageButton: 'a[href^="https://mailtrain.org"]'
|
unsubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/unsubscribe/"]`,
|
||||||
|
manageLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/manage/"]`
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
unsubscribeLink: `/subscription/${list.cid}/unsubscribe/:ucid`,
|
||||||
|
manageLink: `/subscription/${list.cid}/manage/:ucid`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
mailSubscriptionConfirmed: Object.assign(page(driver), mail, {
|
webManage: web({
|
||||||
elementToWaitFor: 'unsubscribeLink',
|
url: `/subscription/${list.cid}/manage/:ucid`,
|
||||||
|
elementsToWaitFor: ['form'],
|
||||||
|
textsToWaitFor: ['Update Your Preferences'],
|
||||||
elements: {
|
elements: {
|
||||||
unsubscribeLink: 'a[href*="/unsubscribe/"]',
|
form: `form[action="/subscription/${list.cid}/manage"]`,
|
||||||
manageLink: 'a[href*="/manage/"]'
|
emailInput: '#main-form input[name="email"]',
|
||||||
|
firstNameInput: '#main-form input[name="first-name"]',
|
||||||
|
lastNameInput: '#main-form input[name="last-name"]',
|
||||||
|
submitButton: 'a[href="#submit"]',
|
||||||
|
manageAddressLink: `a[href^="/subscription/${list.cid}/manage-address/"]`
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
manageAddressLink: `/subscription/${list.cid}/manage-address/:ucid`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
webUnsubscribe: Object.assign(page(driver), web, {
|
webManageAddress: web({
|
||||||
elementToWaitFor: 'submitButton',
|
url: `/subscription/${list.cid}/manage-address/:ucid`,
|
||||||
|
elementsToWaitFor: ['form'],
|
||||||
|
textsToWaitFor: ['Update Your Email Address'],
|
||||||
elements: {
|
elements: {
|
||||||
|
form: `form[action="/subscription/${list.cid}/manage-address"]`,
|
||||||
|
emailInput: '#main-form input[name="email"]',
|
||||||
|
emailNewInput: '#main-form input[name="email-new"]',
|
||||||
submitButton: 'a[href="#submit"]'
|
submitButton: 'a[href="#submit"]'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
webUnsubscribedNotice: Object.assign(page(driver), web, {
|
mailConfirmAddressChange: mail({
|
||||||
elementToWaitFor: 'homepageButton',
|
elementsToWaitFor: ['confirmLink'],
|
||||||
|
textsToWaitFor: ['Please Confirm Subscription Address Change'],
|
||||||
elements: {
|
elements: {
|
||||||
homepageButton: 'a[href^="https://mailtrain.org"]'
|
confirmLink: `a[href^="${config.settings['service-url']}subscription/confirm/change-address/"]`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
mailUnsubscriptionConfirmed: Object.assign(page(driver), mail, {
|
webUpdatedNotice: web({
|
||||||
elementToWaitFor: 'resubscribeLink',
|
url: `/subscription/${list.cid}/updated-notice`,
|
||||||
|
textsToWaitFor: ['Profile Updated']
|
||||||
|
}),
|
||||||
|
|
||||||
|
webUnsubscribedNotice: web({
|
||||||
|
url: `/subscription/${list.cid}/unsubscribed-notice`,
|
||||||
|
textsToWaitFor: ['Unsubscribe Successful']
|
||||||
|
}),
|
||||||
|
|
||||||
|
mailUnsubscriptionConfirmed: mail({
|
||||||
|
elementsToWaitFor: ['resubscribeLink'],
|
||||||
|
textsToWaitFor: ['You Are Now Unsubscribed'],
|
||||||
elements: {
|
elements: {
|
||||||
resubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}"]`
|
resubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}"]`
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
webUnsubscribe: web({
|
||||||
|
elementsToWaitFor: ['submitButton'],
|
||||||
|
textsToWaitFor: ['Unsubscribe'],
|
||||||
|
elements: {
|
||||||
|
submitButton: 'a[href="#submit"]'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
webConfirmUnsubscriptionNotice: web({
|
||||||
|
url: `/subscription/${list.cid}/confirm-unsubscription-notice`,
|
||||||
|
textsToWaitFor: ['We need to confirm your email address']
|
||||||
|
}),
|
||||||
|
|
||||||
|
mailConfirmUnsubscription: mail({
|
||||||
|
elementsToWaitFor: ['confirmLink'],
|
||||||
|
textsToWaitFor: ['Please Confirm Unsubscription'],
|
||||||
|
elements: {
|
||||||
|
confirmLink: `a[href^="${config.settings['service-url']}subscription/confirm/unsubscribe/"]`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
webManualUnsubscribeNotice: web({
|
||||||
|
url: `/subscription/${list.cid}/manual-unsubscribe-notice`,
|
||||||
|
elementsToWaitFor: ['contactLink'],
|
||||||
|
textsToWaitFor: ['Online Unsubscription Is Not Possible', config.settings['admin-email']],
|
||||||
|
elements: {
|
||||||
|
contactLink: `a[href^="mailto:${config.settings['admin-email']}"]`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,32 +1,29 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const page = require('./page');
|
const web = require('../lib/web');
|
||||||
|
|
||||||
module.exports = driver => ({
|
module.exports = {
|
||||||
|
login: web({
|
||||||
login: Object.assign(page(driver), {
|
|
||||||
url: '/users/login',
|
url: '/users/login',
|
||||||
elementToWaitFor: 'submitButton',
|
elementsToWaitFor: ['submitButton'],
|
||||||
elements: {
|
elements: {
|
||||||
usernameInput: 'form[action="/users/login"] input[name="username"]',
|
usernameInput: 'form[action="/users/login"] input[name="username"]',
|
||||||
passwordInput: 'form[action="/users/login"] input[name="password"]',
|
passwordInput: 'form[action="/users/login"] input[name="password"]',
|
||||||
submitButton: 'form[action="/users/login"] [type=submit]'
|
submitButton: 'form[action="/users/login"] [type=submit]'
|
||||||
},
|
|
||||||
enterUsername(value) {
|
|
||||||
// this.element('usernameInput').clear();
|
|
||||||
return this.element('usernameInput').sendKeys(value);
|
|
||||||
},
|
|
||||||
enterPassword(value) {
|
|
||||||
return this.element('passwordInput').sendKeys(value);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
account: Object.assign(page(driver), {
|
logout: web({
|
||||||
|
requestUrl: '/users/logout',
|
||||||
|
url: '/'
|
||||||
|
}),
|
||||||
|
|
||||||
|
account: web({
|
||||||
url: '/users/account',
|
url: '/users/account',
|
||||||
elementToWaitFor: 'emailInput',
|
elementsToWaitFor: ['form'],
|
||||||
elements: {
|
elements: {
|
||||||
|
form: 'form[action="/users/account"]',
|
||||||
emailInput: 'form[action="/users/account"] input[name="email"]'
|
emailInput: 'form[action="/users/account"] input[name="email"]'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
};
|
||||||
});
|
|
|
@ -1,57 +1,70 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const config = require('../helpers/config');
|
/* eslint-disable prefer-arrow-callback */
|
||||||
|
|
||||||
|
const config = require('../lib/config');
|
||||||
|
const { useCase, step, driver } = require('../lib/mocha-e2e');
|
||||||
const expect = require('chai').expect;
|
const expect = require('chai').expect;
|
||||||
const driver = require('../helpers/driver');
|
|
||||||
const home = require('../page-objects/home')(driver);
|
|
||||||
const flash = require('../page-objects/flash')(driver);
|
|
||||||
const {
|
|
||||||
login,
|
|
||||||
account
|
|
||||||
} = require('../page-objects/users')(driver);
|
|
||||||
|
|
||||||
describe('login', function() {
|
const page = require('../page-objects/user');
|
||||||
this.timeout(10000);
|
const home = require('../page-objects/home');
|
||||||
|
|
||||||
|
suite('Login use-cases', () => {
|
||||||
before(() => driver.manage().deleteAllCookies());
|
before(() => driver.manage().deleteAllCookies());
|
||||||
|
|
||||||
it('can access home page', async () => {
|
test('User can access home page', async () => {
|
||||||
await home.navigate();
|
await home.navigate();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can not access restricted content', async () => {
|
test('Anonymous user cannot access restricted content', async () => {
|
||||||
driver.navigate().to(config.baseUrl + '/settings');
|
await driver.navigate().to(config.baseUrl + '/settings');
|
||||||
flash.waitUntilVisible();
|
await page.login.waitUntilVisible();
|
||||||
expect(await flash.getText()).to.contain('Need to be logged in to access restricted content');
|
await page.login.waitForFlash();
|
||||||
await flash.clear();
|
expect(await page.login.getFlash()).to.contain('Need to be logged in to access restricted content');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can not login with false credentials', async () => {
|
useCase('Login (invalid credential)', async () => {
|
||||||
login.enterUsername(config.users.admin.username);
|
await step('User navigates to the login page.', async () => {
|
||||||
login.enterPassword('invalid');
|
await page.login.navigate();
|
||||||
login.submit();
|
|
||||||
flash.waitUntilVisible();
|
|
||||||
expect(await flash.getText()).to.contain('Incorrect username or password');
|
|
||||||
await flash.clear();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can login as admin', async () => {
|
await step('User fills in the user name and incorrect password.', async () => {
|
||||||
login.enterUsername(config.users.admin.username);
|
await page.login.setValue('usernameInput', config.users.admin.username);
|
||||||
login.enterPassword(config.users.admin.password);
|
await page.login.setValue('passwordInput', 'invalid');
|
||||||
login.submit();
|
await page.login.submit();
|
||||||
flash.waitUntilVisible();
|
|
||||||
expect(await flash.getText()).to.contain('Logged in as admin');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can access account page as admin', async () => {
|
await step('System shows a flash notice that credentials are invalid.', async () => {
|
||||||
await account.navigate();
|
await page.login.waitForFlash();
|
||||||
|
expect(await page.login.getFlash()).to.contain('Incorrect username or password');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can logout', async () => {
|
useCase('Login and logout', async () => {
|
||||||
driver.navigate().to(config.baseUrl + '/users/logout');
|
await step('User navigates to the login page.', async () => {
|
||||||
flash.waitUntilVisible();
|
await page.login.navigate();
|
||||||
expect(await flash.getText()).to.contain('logged out');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => driver.quit());
|
await step('User fills in the user name and password.', async () => {
|
||||||
|
await page.login.setValue('usernameInput', config.users.admin.username);
|
||||||
|
await page.login.setValue('passwordInput', config.users.admin.password);
|
||||||
|
await page.login.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows the home page and a flash notice that user has been logged in.', async () => {
|
||||||
|
await home.waitUntilVisibleAfterRefresh();
|
||||||
|
await home.waitForFlash();
|
||||||
|
expect(await home.getFlash()).to.contain('Logged in as admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User navigates to its account.', async () => {
|
||||||
|
await page.account.navigate();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User logs out.', async () => {
|
||||||
|
await page.logout.navigate();
|
||||||
|
await home.waitForFlash();
|
||||||
|
expect(await home.getFlash()).to.contain('logged out');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,101 +1,439 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const config = require('../helpers/config');
|
/* eslint-disable prefer-arrow-callback */
|
||||||
|
|
||||||
|
const config = require('../lib/config');
|
||||||
|
const { useCase, step, precondition, driver } = require('../lib/mocha-e2e');
|
||||||
const shortid = require('shortid');
|
const shortid = require('shortid');
|
||||||
const expect = require('chai').expect;
|
const expect = require('chai').expect;
|
||||||
const driver = require('../helpers/driver');
|
const createPage = require('../page-objects/subscription');
|
||||||
|
|
||||||
const page = require('../page-objects/page')(driver);
|
function getPage(listConf) {
|
||||||
const flash = require('../page-objects/flash')(driver);
|
return createPage(listConf);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
function generateEmail() {
|
||||||
webSubscribe,
|
return 'keep.' + shortid.generate() + '@mailtrain.org';
|
||||||
webConfirmSubscriptionNotice,
|
}
|
||||||
mailConfirmSubscription,
|
|
||||||
webSubscribedNotice,
|
|
||||||
mailSubscriptionConfirmed,
|
|
||||||
webUnsubscribe,
|
|
||||||
webUnsubscribedNotice,
|
|
||||||
mailUnsubscriptionConfirmed
|
|
||||||
} = require('../page-objects/subscription')(driver, config.lists.one);
|
|
||||||
|
|
||||||
const testuser = {
|
async function subscribe(listConf, subscription) {
|
||||||
email: 'keep.' + shortid.generate() + '@mailtrain.org'
|
const page = getPage(listConf);
|
||||||
};
|
|
||||||
|
|
||||||
// console.log(testuser.email);
|
await step('User navigates to list subscription page.', async () => {
|
||||||
|
await page.webSubscribe.navigate();
|
||||||
|
});
|
||||||
|
|
||||||
describe('subscribe (list one)', function() {
|
await step('User submits a valid email and other subscription info.', async () => {
|
||||||
this.timeout(10000);
|
await page.webSubscribe.setValue('emailInput', subscription.email);
|
||||||
|
|
||||||
|
if (subscription.firstName) {
|
||||||
|
await page.webSubscribe.setValue('firstNameInput', subscription.firstName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription.lastName) {
|
||||||
|
await page.webSubscribe.setValue('lastNameInput', subscription.lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.webSubscribe.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows a notice that further instructions are in the email.', async () => {
|
||||||
|
await page.webConfirmSubscriptionNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with a link to confirm the subscription.', async () => {
|
||||||
|
await page.mailConfirmSubscription.fetchMail(subscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks confirm subscription in the email', async () => {
|
||||||
|
await page.mailConfirmSubscription.click('confirmLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows a notice that subscription has been confirmed.', async () => {
|
||||||
|
await page.webSubscribedNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with subscription confirmation.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.fetchMail(subscription.email);
|
||||||
|
subscription.unsubscribeLink = await page.mailSubscriptionConfirmed.getHref('unsubscribeLink');
|
||||||
|
subscription.manageLink = await page.mailSubscriptionConfirmed.getHref('manageLink');
|
||||||
|
|
||||||
|
const unsubscribeParams = await page.mailSubscriptionConfirmed.getLinkParams('unsubscribeLink');
|
||||||
|
const manageParams = await page.mailSubscriptionConfirmed.getLinkParams('manageLink');
|
||||||
|
expect(unsubscribeParams.ucid).to.equal(manageParams.ucid);
|
||||||
|
subscription.ucid = unsubscribeParams.ucid;
|
||||||
|
});
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscriptionExistsPrecondition(listConf, subscription) {
|
||||||
|
await precondition('Subscription exists', 'Subscription to a public list (main scenario)', async () => {
|
||||||
|
await subscribe(listConf, subscription);
|
||||||
|
});
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
suite('Subscription use-cases', () => {
|
||||||
before(() => driver.manage().deleteAllCookies());
|
before(() => driver.manage().deleteAllCookies());
|
||||||
|
|
||||||
it('visits web-subscribe', async () => {
|
useCase('Subscription to a public list (main scenario)', async () => {
|
||||||
await webSubscribe.navigate();
|
await subscribe(config.lists.l1, {
|
||||||
|
email: generateEmail()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submits invalid email (error)', async () => {
|
useCase('Subscription to a public list (invalid email)', async () => {
|
||||||
webSubscribe.enterEmail('foo@bar.nope');
|
const page = getPage(config.lists.l1);
|
||||||
webSubscribe.submit();
|
|
||||||
flash.waitUntilVisible();
|
await step('User navigates to list subscribe page', async () => {
|
||||||
expect(await flash.getText()).to.contain('Invalid email address');
|
await page.webSubscribe.navigate();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submits valid email', async () => {
|
await step('User submits an invalid email.', async () => {
|
||||||
webSubscribe.enterEmail(testuser.email);
|
await page.webSubscribe.setValue('emailInput', 'foo@bar.nope');
|
||||||
await webSubscribe.submit();
|
await page.webSubscribe.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sees web-confirm-subscription-notice', async () => {
|
await step('System shows a flash notice that email is invalid.', async () => {
|
||||||
webConfirmSubscriptionNotice.waitUntilVisible();
|
await page.webSubscribe.waitForFlash();
|
||||||
expect(await page.containsText('Almost Finished')).to.be.true;
|
expect(await page.webSubscribe.getFlash()).to.contain('Invalid email address');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('receives mail-confirm-subscription', async () => {
|
useCase('Subscription to a public list (email already registered)', async () => {
|
||||||
mailConfirmSubscription.navigate(testuser.email);
|
const page = getPage(config.lists.l1);
|
||||||
expect(await page.containsText('Please Confirm Subscription')).to.be.true;
|
|
||||||
|
const subscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||||
|
email: generateEmail()
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicks confirm subscription', async () => {
|
await step('User navigates to list subscribe page', async () => {
|
||||||
await mailConfirmSubscription.click('confirmLink');
|
await page.webSubscribe.navigate();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sees web-subscribed-notice', async () => {
|
await step('User submits the email which has been already registered.', async () => {
|
||||||
webSubscribedNotice.waitUntilVisible();
|
await page.webSubscribe.setValue('emailInput', subscription.email);
|
||||||
expect(await page.containsText('Subscription Confirmed')).to.be.true;
|
await page.webSubscribe.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('receives mail-subscription-confirmed', async () => {
|
await step('System shows a notice that further instructions are in the email.', async () => {
|
||||||
mailSubscriptionConfirmed.navigate(testuser.email);
|
await page.webConfirmSubscriptionNotice.waitUntilVisibleAfterRefresh();
|
||||||
expect(await page.containsText('Subscription Confirmed')).to.be.true;
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
await step('System sends an email informing that the address has been already registered.', async () => {
|
||||||
describe('unsubscribe (list one)', function() {
|
await page.mailAlreadySubscribed.fetchMail(subscription.email);
|
||||||
this.timeout(10000);
|
});
|
||||||
|
|
||||||
it('clicks unsubscribe', async () => {
|
});
|
||||||
await mailSubscriptionConfirmed.click('unsubscribeLink');
|
|
||||||
});
|
useCase('Subscription to a non-public list', async () => {
|
||||||
|
const page = getPage(config.lists.l6);
|
||||||
it('sees web-unsubscribe', async () => {
|
|
||||||
webUnsubscribe.waitUntilVisible();
|
await step('User navigates to list subscription page and sees message that this list does not allow public subscriptions.', async () => {
|
||||||
expect(await page.containsText('Unsubscribe')).to.be.true;
|
await page.webSubscribeNonPublic.navigate();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
it('clicks confirm unsubscription', async () => {
|
|
||||||
await webUnsubscribe.submit();
|
useCase('Change profile info', async () => {
|
||||||
});
|
const page = getPage(config.lists.l1);
|
||||||
|
|
||||||
it('sees web-unsubscribed-notice', async () => {
|
const subscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||||
webUnsubscribedNotice.waitUntilVisible();
|
email: generateEmail(),
|
||||||
expect(await page.containsText('Unsubscribe Successful')).to.be.true;
|
firstName: 'John',
|
||||||
});
|
lastName: 'Doe'
|
||||||
|
});
|
||||||
it('receives mail-unsubscription-confirmed', async () => {
|
|
||||||
mailUnsubscriptionConfirmed.navigate(testuser.email);
|
await step('User clicks the manage subscription button.', async () => {
|
||||||
expect(await page.containsText('You Are Now Unsubscribed')).to.be.true;
|
await page.mailSubscriptionConfirmed.click('manageLink');
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => driver.quit());
|
await step('Systems shows a form to change subscription details. The form contains data entered during subscription.', async () => {
|
||||||
|
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||||
|
expect(await page.webManage.getValue('emailInput')).to.equal(subscription.email);
|
||||||
|
expect(await page.webManage.getValue('firstNameInput')).to.equal(subscription.firstName);
|
||||||
|
expect(await page.webManage.getValue('lastNameInput')).to.equal(subscription.lastName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User enters another name and submits the form.', async () => {
|
||||||
|
subscription.firstName = 'Adam';
|
||||||
|
subscription.lastName = 'B';
|
||||||
|
await page.webManage.setValue('firstNameInput', subscription.firstName);
|
||||||
|
await page.webManage.setValue('lastNameInput', subscription.lastName);
|
||||||
|
await page.webManage.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows a notice that profile has been updated.', async () => {
|
||||||
|
await page.webUpdatedNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User navigates to manage subscription again.', async () => {
|
||||||
|
// await page.webManage.navigate(subscription.manageLink);
|
||||||
|
await page.webManage.navigate({ ucid: subscription.ucid });
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows a form with the changes made previously.', async () => {
|
||||||
|
expect(await page.webManage.getValue('emailInput')).to.equal(subscription.email);
|
||||||
|
expect(await page.webManage.getValue('firstNameInput')).to.equal(subscription.firstName);
|
||||||
|
expect(await page.webManage.getValue('lastNameInput')).to.equal(subscription.lastName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useCase('Change email', async () => {
|
||||||
|
const page = getPage(config.lists.l1);
|
||||||
|
|
||||||
|
const subscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||||
|
email: generateEmail(),
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe'
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the manage subscription button.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.click('manageLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows a form to change subscription details. The form contains data entered during subscription.', async () => {
|
||||||
|
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||||
|
expect(await page.webManage.getValue('emailInput')).to.equal(subscription.email);
|
||||||
|
expect(await page.webManage.getValue('firstNameInput')).to.equal(subscription.firstName);
|
||||||
|
expect(await page.webManage.getValue('lastNameInput')).to.equal(subscription.lastName);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 a new email address and submits the form.', async () => {
|
||||||
|
subscription.email = generateEmail();
|
||||||
|
await page.webManageAddress.setValue('emailNewInput', subscription.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(subscription.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.', 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(subscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with subscription confirmation.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.fetchMail(subscription.email);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useCase('Unsubscription from list #1 (one-step, no form).', async () => {
|
||||||
|
const page = getPage(config.lists.l1);
|
||||||
|
|
||||||
|
const subscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||||
|
email: generateEmail()
|
||||||
|
});
|
||||||
|
|
||||||
|
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(subscription.email);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useCase('Unsubscription from list #2 (one-step, with form).', async () => {
|
||||||
|
const page = getPage(config.lists.l2);
|
||||||
|
|
||||||
|
const subscription = await subscriptionExistsPrecondition(config.lists.l2, {
|
||||||
|
email: generateEmail()
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the unsubscribe button.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows a form to unsubscribe.', async () => {
|
||||||
|
await page.webUnsubscribe.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User confirms unsubscribe and clicks the unsubscribe button.', async () => {
|
||||||
|
await page.webUnsubscribe.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
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(subscription.email);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useCase('Unsubscription from list #3 (two-step, no form).', async () => {
|
||||||
|
const page = getPage(config.lists.l3);
|
||||||
|
|
||||||
|
const subscription = await subscriptionExistsPrecondition(config.lists.l3, {
|
||||||
|
email: generateEmail()
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the unsubscribe button.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows a notice that further instructions are in the email.', async () => {
|
||||||
|
await page.webConfirmUnsubscriptionNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with a link to confirm unsubscription.', async () => {
|
||||||
|
await page.mailConfirmUnsubscription.fetchMail(subscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the confirm unsubscribe button in the email.', async () => {
|
||||||
|
await page.mailConfirmUnsubscription.click('confirmLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
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(subscription.email);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useCase('Unsubscription from list #4 (two-step, with form).', async () => {
|
||||||
|
const page = getPage(config.lists.l4);
|
||||||
|
|
||||||
|
const subscription = await subscriptionExistsPrecondition(config.lists.l4, {
|
||||||
|
email: generateEmail()
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the unsubscribe button.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows a form to unsubscribe.', async () => {
|
||||||
|
await page.webUnsubscribe.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User confirms unsubscribe and clicks the unsubscribe button.', async () => {
|
||||||
|
await page.webUnsubscribe.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows a notice that further instructions are in the email.', async () => {
|
||||||
|
await page.webConfirmUnsubscriptionNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with a link to confirm unsubscription.', async () => {
|
||||||
|
await page.mailConfirmUnsubscription.fetchMail(subscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the confirm unsubscribe button in the email.', async () => {
|
||||||
|
await page.mailConfirmUnsubscription.click('confirmLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
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(subscription.email);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useCase('Unsubscription from list #5 (manual unsubscribe).', async () => {
|
||||||
|
const page = getPage(config.lists.l5);
|
||||||
|
|
||||||
|
await subscriptionExistsPrecondition(config.lists.l5, {
|
||||||
|
email: generateEmail()
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the unsubscribe button.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows a notice that online unsubscription is not possible.', async () => {
|
||||||
|
await page.webManualUnsubscribeNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useCase('Resubscription.', async () => {
|
||||||
|
const page = getPage(config.lists.l1);
|
||||||
|
|
||||||
|
const subscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||||
|
email: generateEmail(),
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe'
|
||||||
|
});
|
||||||
|
|
||||||
|
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(subscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the resubscribe button.', async () => {
|
||||||
|
await page.mailUnsubscriptionConfirmed.click('resubscribeLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows the subscription form. The form contains data entered during initial subscription.', async () => {
|
||||||
|
await page.webSubscribe.waitUntilVisibleAfterRefresh();
|
||||||
|
expect(await page.webSubscribe.getValue('emailInput')).to.equal(subscription.email);
|
||||||
|
expect(await page.webSubscribe.getValue('firstNameInput')).to.equal(subscription.firstName);
|
||||||
|
expect(await page.webSubscribe.getValue('lastNameInput')).to.equal(subscription.lastName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User submits the subscription form.', async () => {
|
||||||
|
await page.webSubscribe.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows a notice that further instructions are in the email.', async () => {
|
||||||
|
await page.webConfirmSubscriptionNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with a link to confirm the subscription.', async () => {
|
||||||
|
await page.mailConfirmSubscription.fetchMail(subscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks confirm subscription in the email', async () => {
|
||||||
|
await page.mailConfirmSubscription.click('confirmLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows a notice that subscription has been confirmed.', async () => {
|
||||||
|
await page.webSubscribedNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with subscription confirmation. The manage and unsubscribe links are identical with the initial subscription.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.fetchMail(subscription.email);
|
||||||
|
const unsubscribeLink = await page.mailSubscriptionConfirmed.getHref('unsubscribeLink');
|
||||||
|
const manageLink = await page.mailSubscriptionConfirmed.getHref('manageLink');
|
||||||
|
expect(subscription.unsubscribeLink).to.equal(unsubscribeLink);
|
||||||
|
expect(subscription.manageLink).to.equal(manageLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -84,7 +84,15 @@
|
||||||
<div class="col-sm-offset-2">
|
<div class="col-sm-offset-2">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}}
|
<input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-offset-2">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -104,7 +104,15 @@
|
||||||
<div class="col-sm-offset-2">
|
<div class="col-sm-offset-2">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}}
|
<input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-offset-2">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -110,7 +110,15 @@
|
||||||
<div class="col-sm-offset-2">
|
<div class="col-sm-offset-2">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}}
|
<input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-offset-2">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -111,7 +111,15 @@
|
||||||
<div class="col-sm-offset-2">
|
<div class="col-sm-offset-2">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}}
|
<input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-offset-2">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -103,7 +103,15 @@
|
||||||
<div class="col-sm-offset-2">
|
<div class="col-sm-offset-2">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}}
|
<input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-offset-2">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -121,7 +121,15 @@
|
||||||
<div class="col-sm-offset-2">
|
<div class="col-sm-offset-2">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}}
|
<input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-offset-2">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -164,7 +164,7 @@
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
{{#unless trackingDisabled}}
|
{{#unless openTrackingDisabled}}
|
||||||
|
|
||||||
<dt>{{#translate}}Opened{{/translate}} <a href="/campaigns/opened/{{id}}" title="{{#translate}}List subscribers who opened this message{{/translate}}"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
<dt>{{#translate}}Opened{{/translate}} <a href="/campaigns/opened/{{id}}" title="{{#translate}}List subscribers who opened this message{{/translate}}"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
@ -174,7 +174,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
|
{{/unless}}
|
||||||
|
{{#unless clickTrackingDisabled}}
|
||||||
<dt>{{#translate}}Clicked{{/translate}} <a href="/campaigns/clicked/{{id}}/all" title="{{#translate}}List subscribers who clicked on a link{{/translate}}"> <span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
<dt>{{#translate}}Clicked{{/translate}} <a href="/campaigns/clicked/{{id}}/all" title="{{#translate}}List subscribers who clicked on a link{{/translate}}"> <span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
|
|
|
@ -59,14 +59,7 @@
|
||||||
|
|
||||||
|
|
||||||
<div id="gjs-wrapper">
|
<div id="gjs-wrapper">
|
||||||
<div id="gjs" style="height:0px; overflow:hidden">
|
<div id="gjs" style="height: 0px; overflow: hidden"></div>
|
||||||
{{#if resource.editorData.html}}
|
|
||||||
<style>{{{resource.editorData.css}}}</style>
|
|
||||||
{{{resource.editorData.html}}}
|
|
||||||
{{else}}
|
|
||||||
{{{resource.html}}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,21 +129,69 @@
|
||||||
<script>
|
<script>
|
||||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
||||||
|
|
||||||
var editor = grapesjs.init({
|
var resource = {{{stringifiedResource}}};
|
||||||
|
|
||||||
|
var config = (function(mode) {
|
||||||
|
var c = {
|
||||||
|
clearOnRender: true,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
storageManager: {
|
storageManager: {
|
||||||
type: 'none'
|
type: 'none'
|
||||||
},
|
},
|
||||||
assetManager: {
|
assetManager: {
|
||||||
assets: [],
|
assets: [],
|
||||||
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}',
|
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}',
|
||||||
uploadText: 'Drop images here or click to upload',
|
uploadText: 'Drop images here or click to upload',
|
||||||
},
|
},
|
||||||
container : '#gjs',
|
container : '#gjs',
|
||||||
fromElement: true,
|
fromElement: false,
|
||||||
plugins: ['gjs-preset-newsletter'],
|
plugins: [],
|
||||||
pluginsOpts: {
|
pluginsOpts: {},
|
||||||
'gjs-preset-newsletter': {
|
};
|
||||||
|
|
||||||
|
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',
|
modalLabelImport: 'Paste all your code here below and click import',
|
||||||
modalLabelExport: 'Copy the code and use it wherever you want',
|
modalLabelExport: 'Copy the code and use it wherever you want',
|
||||||
codeViewerTheme: 'material',
|
codeViewerTheme: 'material',
|
||||||
|
@ -163,59 +204,151 @@
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 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);
|
editor.AssetManager.add(data.files);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getPreparedHtml() {
|
function getMjml() {
|
||||||
var imgs = [];
|
var c = config.pluginsOpts['gjs-mjml'];
|
||||||
$('.gjs-pn-buttons > .gjs-pn-btn.fa.fa-desktop').click();
|
return c.preMjml + editor.getHtml() + c.postMjml;
|
||||||
$('.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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 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 mdlClass = 'gjs-mdl-dialog-sm';
|
||||||
var pnm = editor.Panels;
|
var pnm = editor.Panels;
|
||||||
var cmdm = editor.Commands;
|
var cmdm = editor.Commands;
|
||||||
var testContainer = document.getElementById("test-form");
|
|
||||||
var contentEl = testContainer.querySelector('input[name=html]');
|
|
||||||
var md = editor.Modal;
|
var md = editor.Modal;
|
||||||
|
|
||||||
|
|
||||||
|
// Test email command
|
||||||
|
|
||||||
|
var testContainer = document.getElementById('test-form');
|
||||||
|
var testContentEl = testContainer.querySelector('input[name=html]');
|
||||||
|
|
||||||
cmdm.add('send-test', {
|
cmdm.add('send-test', {
|
||||||
run(editor, sender) {
|
run(editor, sender) {
|
||||||
|
// TODO: Show a spinner
|
||||||
|
getPreparedHtml(function(html) {
|
||||||
sender.set('active', 0);
|
sender.set('active', 0);
|
||||||
var modalContent = md.getContentEl();
|
var modalContent = md.getContentEl();
|
||||||
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
||||||
// var cmdGetCode = cmdm.get('gjs-get-inlined-html');
|
testContentEl.value = html;
|
||||||
// contentEl.value = cmdGetCode && cmdGetCode.run(editor);
|
|
||||||
contentEl.value = getPreparedHtml();
|
|
||||||
mdlDialog.className += ' ' + mdlClass;
|
mdlDialog.className += ' ' + mdlClass;
|
||||||
testContainer.style.display = 'block';
|
testContainer.style.display = 'block';
|
||||||
md.setTitle('Test your Newsletter');
|
md.setTitle('Test your Newsletter');
|
||||||
|
@ -224,7 +357,8 @@
|
||||||
md.getModel().once('change:open', function() {
|
md.getModel().once('change:open', function() {
|
||||||
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
|
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
|
||||||
//clean status
|
//clean status
|
||||||
})
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -240,6 +374,7 @@
|
||||||
|
|
||||||
var statusFormElC = document.querySelector('.form-status');
|
var statusFormElC = document.querySelector('.form-status');
|
||||||
var statusFormEl = document.querySelector('.form-status i');
|
var statusFormEl = document.querySelector('.form-status i');
|
||||||
|
|
||||||
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
|
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
|
||||||
.onStart(function() {
|
.onStart(function() {
|
||||||
statusFormEl.className = 'fa fa-refresh anim-spin';
|
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');
|
var mergeTagReferenceContainer = document.getElementById('merge-tag-reference-container');
|
||||||
cmdm.add('open-merge-tag-reference', {
|
cmdm.add('open-merge-tag-reference', {
|
||||||
run(editor, sender) {
|
run(editor, sender) {
|
||||||
|
@ -287,7 +439,9 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Simple warn notifier
|
// Simple warn notifier
|
||||||
|
|
||||||
var origWarn = console.warn;
|
var origWarn = console.warn;
|
||||||
toastr.options = {
|
toastr.options = {
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
|
@ -300,8 +454,10 @@
|
||||||
origWarn(msg);
|
origWarn(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
// Beautify tooltips
|
// Beautify tooltips
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
$('*[title]').each(function() {
|
$('*[title]').each(function() {
|
||||||
var el = $(this);
|
var el = $(this);
|
||||||
var title = el.attr('title').trim();
|
var title = el.attr('title').trim();
|
||||||
|
@ -310,65 +466,6 @@
|
||||||
el.attr('title', '');
|
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>
|
</script>
|
||||||
|
|
|
@ -3,17 +3,28 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>GrapesJS Newsletter Editor</title>
|
<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/material.css">
|
||||||
<link rel="stylesheet" href="/grapejs/dist/css/tooltip.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="/javascript/jquery-2.2.1.min.js"></script>
|
||||||
<script src="/grapejs/dist/js/grapes.min.js"></script>
|
<script src="/grapejs/dist/js/grapes.min.js?v=0.5.41"></script>
|
||||||
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js"></script>
|
<script src="/grapejs/dist/js/toastr.min.js?v=2.1.3"></script>
|
||||||
<script src="/grapejs/dist/js/toastr.min.js"></script>
|
<script src="/grapejs/dist/js/ajaxable.min.js?v=0.2.3"></script>
|
||||||
<script src="/grapejs/dist/js/ajaxable.min.js"></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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue