work in progress on campaign edit

This commit is contained in:
Tomas Bures 2018-08-01 15:30:20 +05:30
parent 0e0fb944e3
commit ade0fc87f2
10 changed files with 231 additions and 81 deletions

View file

@ -2,10 +2,7 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {translate} from 'react-i18next';
Trans,
translate
} from 'react-i18next';
import { import {
NavButton, NavButton,
requiresAuthenticatedUser, requiresAuthenticatedUser,
@ -13,7 +10,6 @@ import {
withPageHelpers withPageHelpers
} from '../lib/page' } from '../lib/page'
import { import {
ACEEditor,
AlignedRow, AlignedRow,
Button, Button,
ButtonRow, ButtonRow,
@ -42,14 +38,18 @@ import {
getTemplateTypes, getTemplateTypes,
getTypeForm getTypeForm
} from '../templates/helpers'; } from '../templates/helpers';
import {ActionLink} from "../lib/bootstrap-components";
import axios from '../lib/axios'; import axios from '../lib/axios';
import styles from "../lib/styles.scss"; import styles from "../lib/styles.scss";
import {getUrl} from "../lib/urls"; import {getUrl} from "../lib/urls";
import {CampaignType, CampaignSource} from "../../../shared/campaigns"; import {
CampaignSource,
CampaignType
} from "../../../shared/campaigns";
import moment from 'moment'; import moment from 'moment';
import {getMailerTypes} from "../send-configurations/helpers"; import {getMailerTypes} from "../send-configurations/helpers";
import {ResourceType} from "../lib/mosaico";
const overridables = ['from_name', 'from_email', 'reply_to', 'subject'];
@translate() @translate()
@withForm @withForm
@ -60,12 +60,44 @@ export default class CUD extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_'); const t = props.t;
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN);
this.mailerTypes = getMailerTypes(props.t); this.mailerTypes = getMailerTypes(props.t);
this.createTitles = {
[CampaignType.REGULAR]: t('Create Regular Campaign'),
[CampaignType.RSS]: t('Create RSS Campaign'),
[CampaignType.TRIGGERED]: t('Create Triggered Campaign'),
};
this.editTitles = {
[CampaignType.REGULAR]: t('Edit Regular Campaign'),
[CampaignType.RSS]: t('Edit RSS Campaign'),
[CampaignType.TRIGGERED]: t('Edit Triggered Campaign'),
};
this.sourceLabels = {
[CampaignSource.TEMPLATE]: t('Template'),
[CampaignSource.CUSTOM_FROM_TEMPLATE]: t('Custom content'),
[CampaignSource.CUSTOM]: t('Custom content'),
[CampaignSource.URL]: t('URL')
};
this.sourceOptions = [];
for (const key in this.sourceLabels) {
this.sourceOptions.push({key, label: this.sourceLabels[key]});
}
this.customTemplateTypeOptions = [];
for (const key of mailtrainConfig.editors) {
this.customTemplateTypeOptions.push({key, label: this.templateTypes[key].typeName});
}
this.state = { this.state = {
showMergeTagReference: false, showMergeTagReference: false,
elementInFullscreen: false, elementInFullscreen: false,
sendConfiguration: null
}; };
this.initForm({ this.initForm({
@ -82,7 +114,7 @@ export default class CUD extends Component {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
wizard: PropTypes.string, wizard: PropTypes.string,
entity: PropTypes.object, entity: PropTypes.object,
type: PropTypes.number.isRequired type: PropTypes.number
} }
onCustomTemplateTypeChanged(mutState, key, oldType, type) { onCustomTemplateTypeChanged(mutState, key, oldType, type) {
@ -91,6 +123,24 @@ export default class CUD extends Component {
} }
} }
onSendConfigurationChanged(newState, key, oldValue, sendConfigurationId) {
newState.sendConfiguration = null;
this.fetchSendConfiguration(sendConfigurationId);
}
@withAsyncErrorHandler
async fetchSendConfiguration(sendConfigurationId) {
this.fetchSendConfigurationId = sendConfigurationId;
const result = await axios.get(getUrl(`rest/send-configurations-public/${sendConfigurationId}`));
if (sendConfigurationId === this.fetchSendConfigurationId) {
this.setState({
sendConfiguration: result.data
});
}
}
componentDidMount() { componentDidMount() {
if (this.props.entity) { if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => { this.getFormValuesFromEntity(this.props.entity, data => {
@ -101,12 +151,12 @@ export default class CUD extends Component {
} }
if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) { if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
data.data_sourceCustom_type = data.data.source.type; data.data_sourceCustom_type = data.data.sourceCustom.type;
data.data_sourceCustom_data = data.data.source.data; data.data_sourceCustom_data = data.data.sourceCustom.data;
data.data_sourceCustom_html = data.data.source.html; data.data_sourceCustom_html = data.data.sourceCustom.html;
data.data_sourceCustom_text = data.data.source.text; data.data_sourceCustom_text = data.data.sourceCustom.text;
this.templateTypes[data.type].afterLoad(data); this.templateTypes[data.data.sourceCustom.type].afterLoad(data);
} else { } else {
data.data_sourceCustom_type = null; data.data_sourceCustom_type = null;
@ -128,12 +178,24 @@ export default class CUD extends Component {
} }
data.useSegmentation = !!data.segment; data.useSegmentation = !!data.segment;
for (const overridable of overridables) {
data[overridable + '_overriden'] = !!data[overridable + '_override'];
}
this.fetchSendConfiguration(data.send_configuration); this.fetchSendConfiguration(data.send_configuration);
}); });
} else { } else {
const data = {};
for (const overridable of overridables) {
data[overridable + '_override'] = '';
data[overridable + '_overriden'] = false;
}
this.populateFormValues({ this.populateFormValues({
...data,
type: this.props.type, type: this.props.type,
name: '', name: '',
@ -143,14 +205,7 @@ export default class CUD extends Component {
useSegmentation: false, useSegmentation: false,
send_configuration: null, send_configuration: null,
namespace: mailtrainConfig.user.namespace, namespace: mailtrainConfig.user.namespace,
from_name_override: '',
from_name_overriden: false,
from_email_override: '',
from_email_overriden: false,
reply_to_override: '',
reply_to_overriden: false,
subject_override: '',
subject_overriden: false,
click_tracking_disabled: false, click_tracking_disabled: false,
open_trackings_disabled: false, open_trackings_disabled: false,
@ -285,7 +340,7 @@ export default class CUD extends Component {
if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) { if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
this.templateTypes[data.data_sourceCustom_type].beforeSave(data); this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
data.data.source = { data.data.sourceCustom = {
type: data.data_sourceCustom_type, type: data.data_sourceCustom_type,
data: data.data_sourceCustom_data, data: data.data_sourceCustom_data,
html: data.data_sourceCustom_html, html: data.data_sourceCustom_html,
@ -301,6 +356,13 @@ export default class CUD extends Component {
data.data.feedUrl = data.data_feedUrl; data.data.feedUrl = data.data_feedUrl;
} }
for (const overridable of overridables) {
if (!data[overridable + '_overriden']) {
data[overridable + '_override'] = null;
}
delete data[overridable + '_overriden'];
}
for (const key in data) { for (const key in data) {
if (key.startsWith('data_')) { if (key.startsWith('data_')) {
delete data[key]; delete data[key];
@ -335,6 +397,8 @@ export default class CUD extends Component {
this.disableForm(); this.disableForm();
console.log(html);
const response = await axios.post(getUrl('rest/html-to-text', { html })); const response = await axios.post(getUrl('rest/html-to-text', { html }));
this.updateFormValue('data_sourceCustom_text', response.data.text); this.updateFormValue('data_sourceCustom_text', response.data.text);
@ -358,7 +422,6 @@ export default class CUD extends Component {
const t = this.props.t; const t = this.props.t;
const isEdit = !!this.props.entity; const isEdit = !!this.props.entity;
const canDelete = isEdit && this.props.entity.permissions.includes('delete'); const canDelete = isEdit && this.props.entity.permissions.includes('delete');
const useSaveAndEditLabel = !isEdit;
let templateEdit = null; let templateEdit = null;
let extraSettings = null; let extraSettings = null;
@ -366,6 +429,15 @@ export default class CUD extends Component {
const sourceTypeKey = this.getFormValue('source'); const sourceTypeKey = this.getFormValue('source');
const campaignTypeKey = this.getFormValue('type'); const campaignTypeKey = this.getFormValue('type');
let sourceEdit;
if (isEdit) {
sourceEdit = <StaticField id="source" className={styles.formDisabled} label={t('Content source')}>{this.sourceLabels[sourceTypeKey]}</StaticField>;
} else {
sourceEdit = <Dropdown id="source" label={t('Content source')} options={this.sourceOptions}/>
}
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) { if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
const templatesColumns = [ const templatesColumns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
@ -375,14 +447,14 @@ export default class CUD extends Component {
{ data: 5, title: t('Namespace') }, { data: 5, title: t('Namespace') },
]; ];
templateEdit = <TableSelect id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} />; let help = null;
if (sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
} else if (sourceTypeKey === CampaignSource.CUSTOM || (isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) { help = t('Selecting a template creates a campaign specific copy from it.');
const customTemplateTypeOptions = [];
for (const key of mailtrainConfig.editors) {
customTemplateTypeOptions.push({key, label: this.templateTypes[key].typeName});
} }
templateEdit = <TableSelect id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
} else if (sourceTypeKey === CampaignSource.CUSTOM || (isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
// TODO: Toggle HTML preview // TODO: Toggle HTML preview
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type'); const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
@ -401,11 +473,11 @@ export default class CUD extends Component {
templateEdit = <div> templateEdit = <div>
{isEdit {isEdit
? ?
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('Type')}> <StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('Custom template editor')}>
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName} {customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
</StaticField> </StaticField>
: :
<Dropdown id="data_sourceCustom_type" label={t('Type')} options={customTemplateTypeOptions}/> <Dropdown id="data_sourceCustom_type" label={t('Type')} options={this.customTemplateTypeOptions}/>
} }
{customTemplateTypeForm} {customTemplateTypeForm}
@ -414,7 +486,7 @@ export default class CUD extends Component {
</div>; </div>;
} else if (sourceTypeKey === CampaignSource.URL) { } else if (sourceTypeKey === CampaignSource.URL) {
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')}/> templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
} }
if (campaignTypeKey === CampaignType.RSS) { if (campaignTypeKey === CampaignType.RSS) {
@ -442,6 +514,37 @@ export default class CUD extends Component {
{ data: 5, title: t('Namespace') } { data: 5, title: t('Namespace') }
]; ];
let sendSettings;
if (this.getFormValue('send_configuration')) {
if (this.state.sendConfiguration) {
sendSettings = [];
const addOverridable = (id, label) => {
sendSettings.push(<CheckBox key={id + '_overriden'} id={id + '_overriden'} label={label} text={t('Override')}/>);
if (this.getFormValue(id + '_overriden')) {
sendSettings.push(<InputField key={id + '_override'} id={id + '_override'}/>);
} else {
sendSettings.push(
<StaticField key={id + '_original'} id={id + '_original'} className={styles.formDisabled}>
{this.state.sendConfiguration[id]}
</StaticField>
);
}
};
addOverridable('from_name', t('"From" name'));
addOverridable('from_email', t('"From" email address'));
addOverridable('reply_to', t('"Reply-to" email address'));
addOverridable('subject', t('"Subject" line'));
} else {
sendSettings = <AlignedRow>{t('Loading send configuration ...')}</AlignedRow>
}
} else {
sendSettings = null;
}
return ( return (
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}> <div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
{canDelete && {canDelete &&
@ -455,41 +558,40 @@ export default class CUD extends Component {
deletedMsg={t('Campaign deleted')}/> deletedMsg={t('Campaign deleted')}/>
} }
<Title>{isEdit ? t('Edit Campaign') : t('Create Campaign')}</Title> <Title>{isEdit ? this.editTitles[this.getFormValue('type')] : this.createTitles[this.getFormValue('type')]}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/> <InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/> <TextArea id="description" label={t('Description')}/>
<TableSelect id="list" label={t('List')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
<CheckBox id="useSegmentation" text={t('Use segmentation')}/>
{this.getFormValue('useSegmentation') &&
<TableSelect id="segment" label={t('Segment')} withHeader dropdown dataUrl='rest/segments-table' columns={segmentsColumns} selectionLabelIndex={1} />
}
{extraSettings} {extraSettings}
<NamespaceSelect/> <NamespaceSelect/>
<hr/>
<TableSelect id="list" label={t('List')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
<CheckBox id="useSegmentation" label={t('Segment')} text={t('Use a particular segment')}/>
{this.getFormValue('useSegmentation') &&
<TableSelect id="segment" withHeader dropdown dataUrl={`rest/segments-table/${this.getFormValue('list')}`} columns={segmentsColumns} selectionLabelIndex={1} />
}
<hr/>
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} /> <TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
<CheckBox id="from_name_overriden" text={t('Override email "From" name')}/> {sendSettings}
{ this.getFormValue('from_name_overriden') && <InputField id="from_name_override" label={t('Email "From" name')}/> }
<CheckBox id="from_email_overriden" text={t('Override email "From" address')}/>
{ this.getFormValue('from_email_overriden') && <InputField id="from_email_override" label={t('Email "From" address')}/> }
<CheckBox id="reply_to_overriden" text={t('Override email "Reply-to" address')}/>
{ this.getFormValue('reply_to_overriden') && <InputField id="reply_to_override" label={t('Email "Reply-to" address')}/> }
<CheckBox id="subject_overriden" text={t('Override email "Subject" line')}/>
{ this.getFormValue('subject_overriden') && <InputField id="subject_override" label={t('Email "Subject" line')}/> }
<hr/>
<CheckBox id="open_trackings_disabled" text={t('Disable opened tracking')}/> <CheckBox id="open_trackings_disabled" text={t('Disable opened tracking')}/>
<CheckBox id="click_tracking_disabled" text={t('Disable clicked tracking')}/> <CheckBox id="click_tracking_disabled" text={t('Disable clicked tracking')}/>
<hr/>
{sourceEdit}
{templateEdit} {templateEdit}
<ButtonRow> <ButtonRow>

View file

@ -2,9 +2,12 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {translate} from 'react-i18next'; import {translate} from 'react-i18next';
import {Icon} from '../lib/bootstrap-components';
import { import {
NavButton, DropdownMenu,
Icon
} from '../lib/bootstrap-components';
import {
MenuLink,
requiresAuthenticatedUser, requiresAuthenticatedUser,
Title, Title,
Toolbar, Toolbar,
@ -131,7 +134,11 @@ export default class List extends Component {
<div> <div>
<Toolbar> <Toolbar>
{this.state.createPermitted && {this.state.createPermitted &&
<NavButton linkTo="/campaigns/create" className="btn-primary" icon="plus" label={t('Create Campaign')}/> <DropdownMenu className="btn-primary" label={t('Create Campaign')}>
<MenuLink to="/campaigns/create-regular">{t('Regular')}</MenuLink>
<MenuLink to="/campaigns/create-rss">{t('RSS')}</MenuLink>
<MenuLink to="/campaigns/create-triggered">{t('Triggered')}</MenuLink>
</DropdownMenu>
} }
</Toolbar> </Toolbar>

View file

@ -221,7 +221,7 @@ function wrapInput(id, htmlId, owner, format, rightContainerClass, label, help,
class StaticField extends Component { class StaticField extends Component {
static propTypes = { static propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
className: PropTypes.string, className: PropTypes.string,
format: PropTypes.string format: PropTypes.string
@ -247,7 +247,7 @@ class StaticField extends Component {
class InputField extends Component { class InputField extends Component {
static propTypes = { static propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
@ -662,7 +662,7 @@ class ButtonRow extends Component {
class TreeTableSelect extends Component { class TreeTableSelect extends Component {
static propTypes = { static propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string,
dataUrl: PropTypes.string, dataUrl: PropTypes.string,
data: PropTypes.array, data: PropTypes.array,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
@ -713,7 +713,7 @@ class TableSelect extends Component {
dropdown: PropTypes.bool, dropdown: PropTypes.bool,
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
format: PropTypes.string, format: PropTypes.string,
disabled: PropTypes.bool disabled: PropTypes.bool

View file

@ -37,6 +37,7 @@ export class MosaicoEditor extends Component {
entity: PropTypes.object, entity: PropTypes.object,
title: PropTypes.string, title: PropTypes.string,
onFullscreenAsync: PropTypes.func, onFullscreenAsync: PropTypes.func,
templateId: PropTypes.number,
templatePath: PropTypes.string, templatePath: PropTypes.string,
initialModel: PropTypes.string, initialModel: PropTypes.string,
initialMetadata: PropTypes.string initialMetadata: PropTypes.string
@ -60,6 +61,7 @@ export class MosaicoEditor extends Component {
const mosaicoData = { const mosaicoData = {
entityTypeId: this.props.entityTypeId, entityTypeId: this.props.entityTypeId,
entityId: this.props.entity.id, entityId: this.props.entity.id,
templateId: this.props.templateId,
templatePath: this.props.templatePath, templatePath: this.props.templatePath,
initialModel: this.props.initialModel, initialModel: this.props.initialModel,
initialMetadata: this.props.initialMetadata initialMetadata: this.props.initialMetadata
@ -96,6 +98,7 @@ export class MosaicoSandbox extends Component {
static propTypes = { static propTypes = {
entityTypeId: PropTypes.string, entityTypeId: PropTypes.string,
entityId: PropTypes.number, entityId: PropTypes.number,
templateId: PropTypes.number,
templatePath: PropTypes.string, templatePath: PropTypes.string,
initialModel: PropTypes.string, initialModel: PropTypes.string,
initialMetadata: PropTypes.string initialMetadata: PropTypes.string
@ -156,7 +159,7 @@ export class MosaicoSandbox extends Component {
const trustedUrlBase = getTrustedUrl(); const trustedUrlBase = getTrustedUrl();
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase)); 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 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); const allPlugins = plugins.concat(window.mosaicoPlugins);

View file

@ -59,6 +59,7 @@
.tableSelectTable.tableSelectTableHidden { .tableSelectTable.tableSelectTableHidden {
visibility: hidden; visibility: hidden;
height: 0px; height: 0px;
margin-top: -15px;
} }
.tableSelectDropdown input[readonly] { .tableSelectDropdown input[readonly] {

View file

@ -6,6 +6,7 @@ import {
AlignedRow, AlignedRow,
CKEditor, CKEditor,
Dropdown, Dropdown,
StaticField,
TableSelect TableSelect
} from "../lib/form"; } from "../lib/form";
import 'brace/mode/text'; import 'brace/mode/text';
@ -19,9 +20,16 @@ import {
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers'; import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
import {getSandboxUrl} from "../lib/urls"; import {getSandboxUrl} from "../lib/urls";
import mailtrainConfig from 'mailtrainConfig'; 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) // The prefix is used to to enable use within other forms (i.e. campaign form)
const templateTypes = {}; const templateTypes = {};
@ -64,8 +72,8 @@ export function getTemplateTypes(t, prefix = '') {
entity={owner.props.entity} entity={owner.props.entity}
initialModel={owner.getFormValue(prefix + 'mosaicoData').model} initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata} initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
templatePath={getSandboxUrl(`mosaico/templates/${owner.getFormValue(prefix + 'mosaicoTemplate')}/index.html`)} templateId={owner.getFormValue(prefix + 'mosaicoTemplate')}
entityTypeId={ResourceType.TEMPLATE} entityTypeId={entityTypeId}
title={t('Mosaico Template Designer')} title={t('Mosaico Template Designer')}
onFullscreenAsync={::owner.setElementInFullscreen}/> onFullscreenAsync={::owner.setElementInFullscreen}/>
</AlignedRow>, </AlignedRow>,
@ -110,11 +118,17 @@ export function getTemplateTypes(t, prefix = '') {
}; };
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates.map(([key, label]) => ({key, label})); const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates.map(([key, label]) => ({key, label}));
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates);
templateTypes.mosaicoWithFsTemplate = { templateTypes.mosaicoWithFsTemplate = {
typeName: t('Mosaico with predefined templates'), typeName: t('Mosaico with predefined templates'),
getTypeForm: (owner, isEdit) => getTypeForm: (owner, isEdit) => {
<Dropdown id={prefix + 'mosaicoFsTemplate'} label={t('Mosaico Template')} options={mosaicoFsTemplatesOptions}/>, if (isEdit) {
return <StaticField id={prefix + 'mosaicoFsTemplate'} className={styles.formDisabled} label={t('Mosaico Template')}>{mosaicoFsTemplatesLabels.get(owner.getFormValue(prefix + 'mosaicoFsTemplate'))}</StaticField>;
} else {
return <Dropdown id={prefix + 'mosaicoFsTemplate'} label={t('Mosaico Template')} options={mosaicoFsTemplatesOptions}/>;
}
},
getHTMLEditor: owner => getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}> <AlignedRow label={t('Template content (HTML)')}>
<MosaicoEditor <MosaicoEditor
@ -123,7 +137,7 @@ export function getTemplateTypes(t, prefix = '') {
initialModel={owner.getFormValue(prefix + 'mosaicoData').model} initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata} initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
templatePath={getSandboxUrl(`public/mosaico/templates/${owner.getFormValue(prefix + 'mosaicoFsTemplate')}/index.html`)} templatePath={getSandboxUrl(`public/mosaico/templates/${owner.getFormValue(prefix + 'mosaicoFsTemplate')}/index.html`)}
entityTypeId={ResourceType.TEMPLATE} entityTypeId={entityTypeId}
title={t('Mosaico Template Designer')} title={t('Mosaico Template Designer')}
onFullscreenAsync={::owner.setElementInFullscreen}/> onFullscreenAsync={::owner.setElementInFullscreen}/>
</AlignedRow>, </AlignedRow>,
@ -140,7 +154,7 @@ export function getTemplateTypes(t, prefix = '') {
mosaicoData: {} mosaicoData: {}
}), }),
afterLoad: data => { afterLoad: data => {
data['mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate; data[prefix + 'mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate;
data[prefix + 'mosaicoData'] = { data[prefix + 'mosaicoData'] = {
metadata: data[prefix + 'data'].metadata, metadata: data[prefix + 'data'].metadata,
model: data[prefix + 'data'].model model: data[prefix + 'data'].model
@ -310,7 +324,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
export function getTypeForm(owner, typeKey, isEdit) { export function getTypeForm(owner, typeKey, isEdit) {
return <div> return <div>
{owner.templateTypes[typeKey].getTypeForm(this, isEdit)} {owner.templateTypes[typeKey].getTypeForm(owner, isEdit)}
</div>; </div>;
} }

View file

@ -1,21 +1,27 @@
'use strict'; 'use strict';
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const hasher = require('node-object-hash')();
const dtHelpers = require('../lib/dt-helpers'); const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const shortid = require('shortid'); const shortid = require('shortid');
const { enforce, filterObject } = require('../lib/helpers');
const shares = require('./shares'); const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers');
const files = require('./files'); const files = require('./files');
const { CampaignSource, CampaignType} = require('../shared/campaigns'); const { CampaignSource, CampaignType} = require('../shared/campaigns');
const segments = require('./segments'); const segments = require('./segments');
const allowedKeysCommon = ['name', 'description', 'list', 'segment', 'namespace', const allowedKeysCommon = ['name', 'description', 'list', 'segment', 'namespace',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled'];
'source', '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]); const allowedKeysUpdate = new Set([...allowedKeysCommon]);
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeysUpdate));
}
async function listDTAjax(context, params) { async function listDTAjax(context, params) {
return await dtHelpers.ajaxListWithPermissions( return await dtHelpers.ajaxListWithPermissions(
context, context,
@ -29,9 +35,13 @@ async function listDTAjax(context, params) {
async function getById(context, id) { async function getById(context, id) {
return await knex.transaction(async tx => { 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(); const entity = await tx('campaigns').where('id', id).first();
entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id); entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id);
entity.data = JSON.parse(entity.data);
return entity; return entity;
}); });
} }
@ -41,6 +51,10 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
if (isCreate) { if (isCreate) {
enforce(entity.type === CampaignType.REGULAR && entity.type === CampaignType.RSS && entity.type === CampaignType.TRIGGERED, 'Unknown campaign type'); 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'); 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); await segments.getByIdTx(tx, context, entity.list, entity.segment);
} }
if (entity.source === CampaignSource.TEMPLATE || (isCreate && entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE)) { entity.data = JSON.stringify(entity.data);
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
}
entity.data = JSON.stringify(data);
} }
async function create(context, entity) { async function create(context, entity) {
@ -126,6 +136,7 @@ async function updateWithConsistencyCheck(context, entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
existing.data = JSON.parse(existing.data);
const existingHash = hash(existing); const existingHash = hash(existing);
if (existingHash !== entity.originalHash) { if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError(); throw new interoperableErrors.ChangedError();
@ -145,6 +156,8 @@ async function remove(context, id) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete'); await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
// FIXME - deal with deletion of dependent entities (files)
await tx('campaigns').where('id', id).del(); await tx('campaigns').where('id', id).del();
await knex.schema.dropTableIfExists('campaign__' + id); await knex.schema.dropTableIfExists('campaign__' + id);
await knex.schema.dropTableIfExists('campaign_tracker__' + id); await knex.schema.dropTableIfExists('campaign_tracker__' + id);
@ -153,6 +166,10 @@ async function remove(context, id) {
module.exports = { module.exports = {
hash,
listDTAjax, listDTAjax,
getById getById,
create,
updateWithConsistencyCheck,
remove
}; };

View file

@ -92,7 +92,7 @@ async function remove(context, id) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'delete'); 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(); await tx('templates').where('id', id).del();
}); });

View file

@ -10,7 +10,7 @@ router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listDTAjax(req.context, req.body)); 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); const campaign = await campaigns.getById(req.context, req.params.campaignId);
campaign.hash = campaigns.hash(campaign); campaign.hash = campaigns.hash(campaign);
return res.json(campaign); return res.json(campaign);

View file

@ -81,6 +81,12 @@ exports.up = (knex, Promise) => (async() => {
editorType = 'ckeditor'; editorType = 'ckeditor';
} }
if (editorType == 'mosaico') {
editorType = 'mosaicoWithFsTemplate';
editorData.mosaicoFsTemplate = editorData.template;
delete editorData.template;
}
campaign.source = CampaignSource.CUSTOM_FROM_TEMPLATE; campaign.source = CampaignSource.CUSTOM_FROM_TEMPLATE;
data.sourceCustom = { data.sourceCustom = {
type: editorType, type: editorType,