{this.state.createPermitted &&
-
+
+ {t('Regular')}
+ {t('RSS')}
+ {t('Triggered')}
+
}
diff --git a/client/src/lib/form.js b/client/src/lib/form.js
index a335083b..2900155a 100644
--- a/client/src/lib/form.js
+++ b/client/src/lib/form.js
@@ -221,7 +221,7 @@ function wrapInput(id, htmlId, owner, format, rightContainerClass, label, help,
class StaticField extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
+ label: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
className: PropTypes.string,
format: PropTypes.string
@@ -247,7 +247,7 @@ class StaticField extends Component {
class InputField extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
+ label: PropTypes.string,
placeholder: PropTypes.string,
type: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
@@ -662,7 +662,7 @@ class ButtonRow extends Component {
class TreeTableSelect extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
+ label: PropTypes.string,
dataUrl: PropTypes.string,
data: PropTypes.array,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
@@ -713,7 +713,7 @@ class TableSelect extends Component {
dropdown: PropTypes.bool,
id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
+ label: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
format: PropTypes.string,
disabled: PropTypes.bool
diff --git a/client/src/lib/mosaico.js b/client/src/lib/mosaico.js
index af64fb90..11b48a75 100644
--- a/client/src/lib/mosaico.js
+++ b/client/src/lib/mosaico.js
@@ -37,6 +37,7 @@ export class MosaicoEditor extends Component {
entity: PropTypes.object,
title: PropTypes.string,
onFullscreenAsync: PropTypes.func,
+ templateId: PropTypes.number,
templatePath: PropTypes.string,
initialModel: PropTypes.string,
initialMetadata: PropTypes.string
@@ -60,6 +61,7 @@ export class MosaicoEditor extends Component {
const mosaicoData = {
entityTypeId: this.props.entityTypeId,
entityId: this.props.entity.id,
+ templateId: this.props.templateId,
templatePath: this.props.templatePath,
initialModel: this.props.initialModel,
initialMetadata: this.props.initialMetadata
@@ -96,6 +98,7 @@ export class MosaicoSandbox extends Component {
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
+ templateId: PropTypes.number,
templatePath: PropTypes.string,
initialModel: PropTypes.string,
initialMetadata: PropTypes.string
@@ -156,7 +159,7 @@ export class MosaicoSandbox extends Component {
const trustedUrlBase = getTrustedUrl();
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase));
const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase));
- const template = this.props.templatePath;
+ const template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath;
const allPlugins = plugins.concat(window.mosaicoPlugins);
diff --git a/client/src/lib/styles.scss b/client/src/lib/styles.scss
index 43ced772..07427dac 100644
--- a/client/src/lib/styles.scss
+++ b/client/src/lib/styles.scss
@@ -59,6 +59,7 @@
.tableSelectTable.tableSelectTableHidden {
visibility: hidden;
height: 0px;
+ margin-top: -15px;
}
.tableSelectDropdown input[readonly] {
diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js
index 7e996040..063c3d06 100644
--- a/client/src/templates/helpers.js
+++ b/client/src/templates/helpers.js
@@ -6,6 +6,7 @@ import {
AlignedRow,
CKEditor,
Dropdown,
+ StaticField,
TableSelect
} from "../lib/form";
import 'brace/mode/text';
@@ -19,9 +20,16 @@ import {
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
import {getSandboxUrl} from "../lib/urls";
import mailtrainConfig from 'mailtrainConfig';
+import {
+ ActionLink,
+ Button
+} from "../lib/bootstrap-components";
+import {Trans} from "react-i18next";
+
+import styles from "../lib/styles.scss";
-export function getTemplateTypes(t, prefix = '') {
+export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEMPLATE) {
// The prefix is used to to enable use within other forms (i.e. campaign form)
const templateTypes = {};
@@ -64,8 +72,8 @@ export function getTemplateTypes(t, prefix = '') {
entity={owner.props.entity}
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
- templatePath={getSandboxUrl(`mosaico/templates/${owner.getFormValue(prefix + 'mosaicoTemplate')}/index.html`)}
- entityTypeId={ResourceType.TEMPLATE}
+ templateId={owner.getFormValue(prefix + 'mosaicoTemplate')}
+ entityTypeId={entityTypeId}
title={t('Mosaico Template Designer')}
onFullscreenAsync={::owner.setElementInFullscreen}/>
,
@@ -110,11 +118,17 @@ export function getTemplateTypes(t, prefix = '') {
};
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates.map(([key, label]) => ({key, label}));
+ const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates);
templateTypes.mosaicoWithFsTemplate = {
typeName: t('Mosaico with predefined templates'),
- getTypeForm: (owner, isEdit) =>
-
,
+ getTypeForm: (owner, isEdit) => {
+ if (isEdit) {
+ return {mosaicoFsTemplatesLabels.get(owner.getFormValue(prefix + 'mosaicoFsTemplate'))};
+ } else {
+ return ;
+ }
+ },
getHTMLEditor: owner =>
,
@@ -140,7 +154,7 @@ export function getTemplateTypes(t, prefix = '') {
mosaicoData: {}
}),
afterLoad: data => {
- data['mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate;
+ data[prefix + 'mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate;
data[prefix + 'mosaicoData'] = {
metadata: data[prefix + 'data'].metadata,
model: data[prefix + 'data'].model
@@ -310,7 +324,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
export function getTypeForm(owner, typeKey, isEdit) {
return
- {owner.templateTypes[typeKey].getTypeForm(this, isEdit)}
+ {owner.templateTypes[typeKey].getTypeForm(owner, isEdit)}
;
}
diff --git a/models/campaigns.js b/models/campaigns.js
index 86375b20..7567235e 100644
--- a/models/campaigns.js
+++ b/models/campaigns.js
@@ -1,21 +1,27 @@
'use strict';
const knex = require('../lib/knex');
+const hasher = require('node-object-hash')();
const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors');
const shortid = require('shortid');
+const { enforce, filterObject } = require('../lib/helpers');
const shares = require('./shares');
+const namespaceHelpers = require('../lib/namespace-helpers');
const files = require('./files');
const { CampaignSource, CampaignType} = require('../shared/campaigns');
const segments = require('./segments');
const allowedKeysCommon = ['name', 'description', 'list', 'segment', 'namespace',
- 'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override',
- 'source', 'data', 'click_tracking_disabled', 'open_tracking_disabled'];
+ 'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled'];
-const allowedKeysCreate = new Set(['type', ...allowedKeysCommon]);
+const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
const allowedKeysUpdate = new Set([...allowedKeysCommon]);
+function hash(entity) {
+ return hasher.hash(filterObject(entity, allowedKeysUpdate));
+}
+
async function listDTAjax(context, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
@@ -29,9 +35,13 @@ async function listDTAjax(context, params) {
async function getById(context, id) {
return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'campaign', 'view');
+ await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
const entity = await tx('campaigns').where('id', id).first();
+
entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id);
+
+ entity.data = JSON.parse(entity.data);
+
return entity;
});
}
@@ -41,6 +51,10 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
if (isCreate) {
enforce(entity.type === CampaignType.REGULAR && entity.type === CampaignType.RSS && entity.type === CampaignType.TRIGGERED, 'Unknown campaign type');
+
+ if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
+ await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
+ }
}
enforce(entity.source >= CampaignSource.MIN && entity.source <= CampaignSource.MAX, 'Unknown campaign source');
@@ -52,11 +66,7 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
await segments.getByIdTx(tx, context, entity.list, entity.segment);
}
- if (entity.source === CampaignSource.TEMPLATE || (isCreate && entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
- await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
- }
-
- entity.data = JSON.stringify(data);
+ entity.data = JSON.stringify(entity.data);
}
async function create(context, entity) {
@@ -126,6 +136,7 @@ async function updateWithConsistencyCheck(context, entity) {
throw new interoperableErrors.NotFoundError();
}
+ existing.data = JSON.parse(existing.data);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError();
@@ -145,6 +156,8 @@ async function remove(context, id) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
+ // FIXME - deal with deletion of dependent entities (files)
+
await tx('campaigns').where('id', id).del();
await knex.schema.dropTableIfExists('campaign__' + id);
await knex.schema.dropTableIfExists('campaign_tracker__' + id);
@@ -153,6 +166,10 @@ async function remove(context, id) {
module.exports = {
+ hash,
listDTAjax,
- getById
+ getById,
+ create,
+ updateWithConsistencyCheck,
+ remove
};
\ No newline at end of file
diff --git a/models/templates.js b/models/templates.js
index eb1d6f9a..07d4fb2c 100644
--- a/models/templates.js
+++ b/models/templates.js
@@ -92,7 +92,7 @@ async function remove(context, id) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'delete');
- // FIXME - deal with deletion of dependent entities
+ // FIXME - deal with deletion of dependent entities (files, etc.)
await tx('templates').where('id', id).del();
});
diff --git a/routes/rest/campaigns.js b/routes/rest/campaigns.js
index a3444b23..9c9df754 100644
--- a/routes/rest/campaigns.js
+++ b/routes/rest/campaigns.js
@@ -10,7 +10,7 @@ router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listDTAjax(req.context, req.body));
});
-router.getAsync('/campaings/:campaignId', passport.loggedIn, async (req, res) => {
+router.getAsync('/campaigns/:campaignId', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, req.params.campaignId);
campaign.hash = campaigns.hash(campaign);
return res.json(campaign);
diff --git a/setup/knex/migrations/20180718220444_upgrade_campaigns.js b/setup/knex/migrations/20180718220444_upgrade_campaigns.js
index 00e99b2c..957076d2 100644
--- a/setup/knex/migrations/20180718220444_upgrade_campaigns.js
+++ b/setup/knex/migrations/20180718220444_upgrade_campaigns.js
@@ -81,6 +81,12 @@ exports.up = (knex, Promise) => (async() => {
editorType = 'ckeditor';
}
+ if (editorType == 'mosaico') {
+ editorType = 'mosaicoWithFsTemplate';
+ editorData.mosaicoFsTemplate = editorData.template;
+ delete editorData.template;
+ }
+
campaign.source = CampaignSource.CUSTOM_FROM_TEMPLATE;
data.sourceCustom = {
type: editorType,