- "Channels" feature
- Shoutout config param rendered on the homepage
- "Clone" feature for campaigns
This commit is contained in:
Tomas Bures 2020-07-17 14:53:48 +02:00
parent 00432e6cfe
commit d170548cfa
25 changed files with 1009 additions and 525 deletions

View file

@ -25,7 +25,7 @@ export default class List extends Component {
return (
<div>
<h2>{t('Mailtrain 2 beta')}</h2>
<div>{t('Build') + ' 2020-05-28-0102'}</div>
<div>{t('Build') + ' 2020-07-17-0000'}</div>
<p>{this.props.configItems.shoutout}</p>
</div>
);

View file

@ -33,7 +33,7 @@ import {getUrl} from "../lib/urls";
import {campaignOverridables, CampaignSource, CampaignStatus, CampaignType} from "../../../shared/campaigns";
import moment from 'moment';
import {getMailerTypes} from "../send-configurations/helpers";
import {getCampaignLabels} from "./helpers";
import {getCampaignLabels, ListsSelectorHelper} from "./helpers";
import {withComponentMixins} from "../lib/decorator-helpers";
import interoperableErrors from "../../../shared/interoperable-errors";
import {Trans} from "react-i18next";
@ -51,6 +51,8 @@ export default class CUD extends Component {
const t = props.t;
this.listsSelectorHelper = new ListsSelectorHelper(this, t, 'lists');
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN);
this.tagLanguages = getTagLanguages(props.t);
@ -79,20 +81,9 @@ export default class CUD extends Component {
[CampaignSource.URL]: t('url')
};
let sourceLabelsOrder;
if (props.createFromChannel) {
// If a campaign is created within a channel, we allow only for those source types that makes sense
sourceLabelsOrder = [
CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_CAMPAIGN, CampaignSource.CUSTOM_FROM_TEMPLATE
];
} else {
// Regular creation or createFromCampaign
sourceLabelsOrder = [
CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_CAMPAIGN , CampaignSource.TEMPLATE, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.URL
];
}
const sourceLabelsOrder = [
CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_CAMPAIGN , CampaignSource.TEMPLATE, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.URL
];
this.sourceOptions = [];
for (const key of sourceLabelsOrder) {
@ -113,8 +104,6 @@ export default class CUD extends Component {
sendConfiguration: null
};
this.nextListEntryId = 0;
this.initForm({
leaveConfirmation: !props.entity || props.entity.permissions.includes('edit'),
onChange: {
@ -128,17 +117,11 @@ export default class CUD extends Component {
action: PropTypes.string.isRequired,
entity: PropTypes.object,
createFromChannel: PropTypes.object,
createFromCampaign: PropTypes.object,
crateFromCampaign: PropTypes.object,
permissions: PropTypes.object,
type: PropTypes.number
}
getNextListEntryId() {
const id = this.nextListEntryId;
this.nextListEntryId += 1;
return id;
}
onFormChangeBeforeValidation(mutStateData, key, oldValue, newValue) {
let match;
@ -156,10 +139,7 @@ export default class CUD extends Component {
}
}
if (key && (match = key.match(/^(lists_[0-9]+_)list$/))) {
const prefix = match[1];
mutStateData.setIn([prefix + 'segment', 'value'], null);
}
this.listsSelectorHelper.onFormChangeBeforeValidation(mutStateData, key, oldValue, newValue);
}
onSendConfigurationChanged(newState, key, oldValue, sendConfigurationId) {
@ -216,19 +196,7 @@ export default class CUD extends Component {
}
}
const lsts = [];
for (const lst of data.lists) {
const lstUid = this.getNextListEntryId();
const prefix = 'lists_' + lstUid + '_';
data[prefix + 'list'] = lst.list;
data[prefix + 'segment'] = lst.segment;
data[prefix + 'useSegmentation'] = !!lst.segment;
lsts.push(lstUid);
}
data.lists = lsts;
this.listsSelectorHelper.getFormValuesMutator(data);
// noinspection JSIgnoredPromiseFromCall
this.fetchSendConfiguration(data.send_configuration);
@ -275,27 +243,10 @@ export default class CUD extends Component {
delete data[overridable + '_overriden'];
}
const lsts = [];
for (const lstUid of data.lists) {
const prefix = 'lists_' + lstUid + '_';
const useSegmentation = data[prefix + 'useSegmentation'];
lsts.push({
list: data[prefix + 'list'],
segment: useSegmentation ? data[prefix + 'segment'] : null
});
}
data.lists = lsts;
for (const key in data) {
if (key.startsWith('data_') || key.startsWith('lists_')) {
delete data[key];
}
}
this.listsSelectorHelper.submitFormValuesMutator(data);
return filterData(data, [
'name', 'description', 'segment', 'namespace', 'send_configuration',
'name', 'description', 'channel', 'namespace', 'send_configuration',
'subject', 'from_name_override', 'from_email_override', 'reply_to_override',
'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url',
'type', 'source', 'parent', 'lists'
@ -314,10 +265,33 @@ export default class CUD extends Component {
const data = {};
// This is for CampaignSource.TEMPLATE and CampaignSource.CUSTOM_FROM_TEMPLATE
data.data_sourceTemplate = null;
// This is for CampaignSource.CUSTOM_FROM_CAMPAIGN
data.data_sourceCampaign = null;
// This is for CampaignSource.CUSTOM
data.data_sourceCustom_type = mailtrainConfig.editors[0];
data.data_sourceCustom_tag_language = mailtrainConfig.tagLanguages[0];
data.data_sourceCustom_data = {};
data.data_sourceCustom_html = '';
data.data_sourceCustom_text = '';
Object.assign(data, this.templateTypes[mailtrainConfig.editors[0]].initData());
// This is for CampaignSource.URL
data.data_sourceUrl = '';
// This is for CampaignType.RSS
data.data_feedUrl = '';
if (this.props.createFromChannel) {
const channel = this.props.createFromChannel;
data.channel = channel.id;
for (const overridable of campaignOverridables) {
if (channel[overridable + '_override'] === null) {
data[overridable + '_override'] = '';
@ -328,19 +302,7 @@ export default class CUD extends Component {
}
}
const lsts = [];
for (const lst of channel.lists) {
const lstUid = this.getNextListEntryId();
const prefix = 'lists_' + lstUid + '_';
data[prefix + 'list'] = lst.list;
data[prefix + 'segment'] = lst.segment;
data[prefix + 'useSegmentation'] = !!lst.segment;
lsts.push(lstUid);
}
data.lists = lsts;
this.listsSelectorHelper.populateFrom(data, channel.lists);
data.type = CampaignType.REGULAR;
@ -366,23 +328,27 @@ export default class CUD extends Component {
if (channel.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
data.data_sourceTemplate = channel.sourceTemplate;
}
if (channel.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
channel.data_sourceCampaign = channel.data.sourceCampaign;
}
} else if (channel.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
data.data_sourceCampaign = channel.data.sourceCampaign;
if (channel.source === CampaignSource.CUSTOM) {
} else if (channel.source === CampaignSource.CUSTOM) {
data.data_sourceCustom_type = channel.data.sourceCustom.type;
data.data_sourceCustom_tag_language = channel.data.sourceCustom.tag_language;
data.data_sourceCustom_data = channel.data.sourceCustom.data;
this.templateTypes[channel.data.sourceCustom.type].afterLoad(data);
} else if (channel.source === CampaignSource.URL) {
data.data_sourceUrl = channel.data.sourceUrl
}
} else if (this.props.createFromCampaign) {
const sourceCampaign = this.props.createFromCampaign;
data.channel = sourceCampaign.channel;
for (const overridable of campaignOverridables) {
if (sourceCampaign[overridable + '_override'] === null) {
data[overridable + '_override'] = '';
@ -393,19 +359,7 @@ export default class CUD extends Component {
}
}
const lsts = [];
for (const lst of sourceCampaign.lists) {
const lstUid = this.getNextListEntryId();
const prefix = 'lists_' + lstUid + '_';
data[prefix + 'list'] = lst.list;
data[prefix + 'segment'] = lst.segment;
data[prefix + 'useSegmentation'] = !!lst.segment;
lsts.push(lstUid);
}
data.lists = lsts;
this.listsSelectorHelper.populateFrom(data, sourceCampaign.lists);
data.type = sourceCampaign.type;
@ -425,28 +379,6 @@ export default class CUD extends Component {
data.unsubscribe_url = sourceCampaign.unsubscribe_url;
// This is for CampaignSource.TEMPLATE and CampaignSource.CUSTOM_FROM_TEMPLATE
data.data_sourceTemplate = null;
// This is for CampaignSource.CUSTOM_FROM_CAMPAIGN
data.data_sourceCampaign = null;
// This is for CampaignSource.CUSTOM
data.data_sourceCustom_type = mailtrainConfig.editors[0];
data.data_sourceCustom_tag_language = mailtrainConfig.tagLanguages[0];
data.data_sourceCustom_data = {};
data.data_sourceCustom_html = '';
data.data_sourceCustom_text = '';
Object.assign(data, this.templateTypes[mailtrainConfig.editors[0]].initData());
// This is for CampaignSource.URL
data.data_sourceUrl = '';
// This is for CampaignType.RSS
data.data_feedUrl = '';
if (sourceCampaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceCampaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN || sourceCampaign.source === CampaignSource.CUSTOM) {
data.source = CampaignSource.CUSTOM_FROM_CAMPAIGN;
data.data_sourceCampaign = sourceCampaign.id;
@ -466,18 +398,14 @@ export default class CUD extends Component {
data[overridable + '_overriden'] = false;
}
data.channel = null;
data.type = this.props.type;
data.name = '';
data.description = '';
const lstUid = this.getNextListEntryId();
const lstPrefix = 'lists_' + lstUid + '_';
data[lstPrefix + 'list'] = null;
data[lstPrefix + 'segment'] = null;
data[lstPrefix + 'useSegmentation'] = false;
data.lists = [lstUid];
this.listsSelectorHelper.populateFrom(data, [{list: null, segment: null}]);
data.send_configuration = null;
data.namespace = getDefaultNamespace(this.props.permissions);
@ -490,27 +418,6 @@ export default class CUD extends Component {
data.unsubscribe_url = '';
data.source = CampaignSource.CUSTOM;
// This is for CampaignSource.TEMPLATE and CampaignSource.CUSTOM_FROM_TEMPLATE
data.data_sourceTemplate = null;
// This is for CampaignSource.CUSTOM_FROM_CAMPAIGN
data.data_sourceCampaign = null;
// This is for CampaignSource.CUSTOM
data.data_sourceCustom_type = mailtrainConfig.editors[0];
data.data_sourceCustom_tag_language = mailtrainConfig.tagLanguages[0];
data.data_sourceCustom_data = {};
data.data_sourceCustom_html = '';
data.data_sourceCustom_text = '';
Object.assign(data, this.templateTypes[mailtrainConfig.editors[0]].initData());
// This is for CampaignSource.URL
data.data_sourceUrl = '';
// This is for CampaignType.RSS
data.data_feedUrl = '';
}
this.populateFormValues(data);
@ -583,17 +490,7 @@ export default class CUD extends Component {
}
}
for (const lstUid of state.getIn(['lists', 'value'])) {
const prefix = 'lists_' + lstUid + '_';
if (!state.getIn([prefix + 'list', 'value'])) {
state.setIn([prefix + 'list', 'error'], t('listMustBeSelected'));
}
if (state.getIn([prefix + 'useSegmentation', 'value']) && !state.getIn([prefix + 'segment', 'value'])) {
state.setIn([prefix + 'segment', 'error'], t('segmentMustBeSelected'));
}
}
this.listsSelectorHelper.localValidateFormValues(state)
validateNamespace(t, state);
}
@ -627,7 +524,12 @@ export default class CUD extends Component {
if (afterSubmitAction === CUD.AfterSubmitAction.STATUS) {
this.navigateToWithFlashMessage(`/campaigns/${this.props.entity.id}/status`, 'success', t('campaignUpdated'));
} else if (afterSubmitAction === CUD.AfterSubmitAction.LEAVE) {
this.navigateToWithFlashMessage('/campaigns', 'success', t('campaignUpdated'));
const channelId = this.getFormValue('channel');
if (channelId) {
this.navigateToWithFlashMessage(`/channels/${channelId}/campaigns`, 'success', t('campaignUpdated'));
} else {
this.navigateToWithFlashMessage('/campaigns', 'success', t('campaignUpdated'));
}
} else {
await this.getFormValuesFromURL(`rest/campaigns-settings/${this.props.entity.id}`);
this.enableForm();
@ -642,7 +544,12 @@ export default class CUD extends Component {
if (afterSubmitAction === CUD.AfterSubmitAction.STATUS) {
this.navigateToWithFlashMessage(`/campaigns/${submitResult}/status`, 'success', t('campaignCreated'));
} else if (afterSubmitAction === CUD.AfterSubmitAction.LEAVE) {
this.navigateToWithFlashMessage(`/campaigns`, 'success', t('campaignCreated'));
const channelId = this.getFormValue('channel');
if (channelId) {
this.navigateToWithFlashMessage(`/channels/${channelId}/campaigns`, 'success', t('campaignCreated'));
} else {
this.navigateToWithFlashMessage(`/campaigns`, 'success', t('campaignCreated'));
}
} else {
this.navigateToWithFlashMessage(`/campaigns/${submitResult}/edit`, 'success', t('campaignCreated'));
}
@ -654,46 +561,6 @@ export default class CUD extends Component {
}
}
onAddListEntry(orderBeforeIdx) {
this.updateForm(mutState => {
const lsts = mutState.getIn(['lists', 'value']);
let paramId = 0;
const lstUid = this.getNextListEntryId();
const prefix = 'lists_' + lstUid + '_';
mutState.setIn([prefix + 'list', 'value'], null);
mutState.setIn([prefix + 'segment', 'value'], null);
mutState.setIn([prefix + 'useSegmentation', 'value'], false);
mutState.setIn(['lists', 'value'], [...lsts.slice(0, orderBeforeIdx), lstUid, ...lsts.slice(orderBeforeIdx)]);
});
}
onRemoveListEntry(lstUid) {
this.updateForm(mutState => {
const lsts = this.getFormValue('lists');
const prefix = 'lists_' + lstUid + '_';
mutState.delete(prefix + 'list');
mutState.delete(prefix + 'segment');
mutState.delete(prefix + 'useSegmentation');
mutState.setIn(['lists', 'value'], lsts.filter(val => val !== lstUid));
});
}
onListEntryMoveUp(orderIdx) {
const lsts = this.getFormValue('lists');
this.updateFormValue('lists', [...lsts.slice(0, orderIdx - 1), lsts[orderIdx], lsts[orderIdx - 1], ...lsts.slice(orderIdx + 1)]);
}
onListEntryMoveDown(orderIdx) {
const lsts = this.getFormValue('lists');
this.updateFormValue('lists', [...lsts.slice(0, orderIdx), lsts[orderIdx + 1], lsts[orderIdx], ...lsts.slice(orderIdx + 2)]);
}
render() {
const t = this.props.t;
@ -710,90 +577,13 @@ export default class CUD extends Component {
extraSettings = <InputField id="data_feedUrl" label={t('rssFeedUrl')}/>
}
const listsColumns = [
const channelsColumns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('subscribers') },
{ data: 4, title: t('description') },
{ data: 5, title: t('namespace') }
{ data: 3, title: t('description') },
{ data: 4, title: t('namespace') }
];
const segmentsColumns = [
{ data: 1, title: t('name') }
];
const lstsEditEntries = [];
const lsts = this.getFormValue('lists') || [];
let lstOrderIdx = 0;
for (const lstUid of lsts) {
const prefix = 'lists_' + lstUid + '_';
const lstOrderIdxClosure = lstOrderIdx;
const selectedList = this.getFormValue(prefix + 'list');
lstsEditEntries.push(
<div key={lstUid} className={campaignsStyles.entry + ' ' + campaignsStyles.entryWithButtons}>
<div className={campaignsStyles.entryButtons}>
{lsts.length > 1 &&
<Button
className="btn-secondary"
icon="trash-alt"
title={t('remove')}
onClickAsync={() => this.onRemoveListEntry(lstUid)}
/>
}
<Button
className="btn-secondary"
icon="plus"
title={t('insertNewEntryBeforeThisOne')}
onClickAsync={() => this.onAddListEntry(lstOrderIdxClosure)}
/>
{lstOrderIdx > 0 &&
<Button
className="btn-secondary"
icon="chevron-up"
title={t('moveUp')}
onClickAsync={() => this.onListEntryMoveUp(lstOrderIdxClosure)}
/>
}
{lstOrderIdx < lsts.length - 1 &&
<Button
className="btn-secondary"
icon="chevron-down"
title={t('moveDown')}
onClickAsync={() => this.onListEntryMoveDown(lstOrderIdxClosure)}
/>
}
</div>
<div className={campaignsStyles.entryContent}>
<TableSelect id={prefix + 'list'} label={t('list')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
<div>
<CheckBox id={prefix + 'useSegmentation'} label={t('segment')} text={t('useAParticularSegment')}/>
{selectedList && this.getFormValue(prefix + 'useSegmentation') &&
<TableSelect id={prefix + 'segment'} withHeader dropdown dataUrl={`rest/segments-table/${selectedList}`} columns={segmentsColumns} selectionLabelIndex={1} />
}
</div>
</div>
</div>
);
lstOrderIdx += 1;
}
const lstsEdit =
<Fieldset label={t('lists')}>
{lstsEditEntries}
<div key="newEntry" className={campaignsStyles.newEntry}>
<Button
className="btn-secondary"
icon="plus"
label={t('addList')}
onClickAsync={() => this.onAddListEntry(lsts.length)}
/>
</div>
</Fieldset>;
const sendConfigurationsColumns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
@ -938,13 +728,15 @@ export default class CUD extends Component {
<TextArea id="description" label={t('description')}/>
<TableSelect id="channel" label={t('Channel')} withHeader withClear dropdown dataUrl='rest/channels-with-create-campaign-permission-table' columns={channelsColumns} selectionLabelIndex={1} />
{extraSettings}
<NamespaceSelect/>
<hr/>
{lstsEdit}
{this.listsSelectorHelper.render()}
<hr/>

View file

@ -171,7 +171,12 @@ export default class CustomContent extends Component {
if (afterSubmitAction === CustomContent.AfterSubmitAction.STATUS) {
this.navigateToWithFlashMessage(`/campaigns/${this.props.entity.id}/status`, 'success', t('campaignUpdated'));
} else if (afterSubmitAction === CustomContent.AfterSubmitAction.LEAVE) {
this.navigateToWithFlashMessage('/campaigns', 'success', t('campaignUpdated'));
const channelId = this.props.entity.channel;
if (channelId) {
this.navigateToWithFlashMessage(`/channels/${channelId}/campaigns`, 'success', t('campaignUpdated'));
} else {
this.navigateToWithFlashMessage('/campaigns', 'success', t('campaignUpdated'));
}
} else {
await this.getFormValuesFromURL(`rest/campaigns-content/${this.props.entity.id}`);
this.enableForm();

View file

@ -44,7 +44,7 @@ export default class List extends Component {
const channel = this.props.channel;
const permissions = this.props.permissions;
const createPermitted = permissions.createCampaign;
const createPermitted = permissions.createCampaign && (!channel || channel.permissions.includes('createCampaign'));
const columns = [];
columns.push({
@ -184,7 +184,11 @@ export default class List extends Component {
<Title>{t('campaigns')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} order={[6, 'desc']} />
{channel ?
<Table ref={node => this.table = node} withHeader dataUrl={`rest/campaigns-by-channel-table/${channel.id}`} columns={columns} order={[5, 'desc']} />
:
<Table ref={node => this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} order={[6, 'desc']} />
}
</div>
);
}

View file

@ -1,6 +1,10 @@
'use strict';
import {CampaignStatus, CampaignType} from "../../../shared/campaigns";
import campaignsStyles from "./styles.scss";
import {Button} from "../lib/bootstrap-components";
import {CheckBox, Fieldset, TableSelect} from "../lib/form";
import React from "react";
export function getCampaignLabels(t) {
@ -30,3 +34,251 @@ export function getCampaignLabels(t) {
}
export class ListsSelectorHelper {
constructor(owner, t, id, allowEmpty = false) {
this.owner = owner;
this.t = t;
this.id = id;
this.nextEntryId = 0;
this.allowEmpty = allowEmpty;
this.keyRegex = new RegExp(`^(${id}_[0-9]+_)list$`);
}
getNextEntryId() {
const id = this.nextEntryId;
this.nextEntryId += 1;
return id;
}
getPrefix(lstUid) {
return this.id + '_' + lstUid + '_';
}
onAddListEntry(orderBeforeIdx) {
const owner = this.owner;
const id = this.id;
owner.updateForm(mutState => {
const lsts = mutState.getIn([id, 'value']);
let paramId = 0;
const lstUid = this.getNextEntryId();
const prefix = this.getPrefix(lstUid);
mutState.setIn([prefix + 'list', 'value'], null);
mutState.setIn([prefix + 'segment', 'value'], null);
mutState.setIn([prefix + 'useSegmentation', 'value'], false);
mutState.setIn([id, 'value'], [...lsts.slice(0, orderBeforeIdx), lstUid, ...lsts.slice(orderBeforeIdx)]);
});
}
onRemoveListEntry(lstUid) {
const owner = this.owner;
const id = this.id;
owner.updateForm(mutState => {
const lsts = owner.getFormValue(id);
const prefix = this.getPrefix(lstUid);
mutState.delete(prefix + 'list');
mutState.delete(prefix + 'segment');
mutState.delete(prefix + 'useSegmentation');
mutState.setIn([id, 'value'], lsts.filter(val => val !== lstUid));
});
}
onListEntryMoveUp(orderIdx) {
const owner = this.owner;
const id = this.id;
const lsts = owner.getFormValue(id);
owner.updateFormValue(id, [...lsts.slice(0, orderIdx - 1), lsts[orderIdx], lsts[orderIdx - 1], ...lsts.slice(orderIdx + 1)]);
}
onListEntryMoveDown(orderIdx) {
const owner = this.owner;
const id = this.id;
const lsts = owner.getFormValue(id);
owner.updateFormValue(id, [...lsts.slice(0, orderIdx), lsts[orderIdx + 1], lsts[orderIdx], ...lsts.slice(orderIdx + 2)]);
}
// Public methods
onFormChangeBeforeValidation(mutStateData, key, oldValue, newValue) {
let match;
if (key && (match = key.match(this.keyRegex))) {
const prefix = match[1];
mutStateData.setIn([prefix + 'segment', 'value'], null);
}
}
getFormValuesMutator(data) {
const id = this.id;
const lsts = [];
for (const lst of data[id]) {
const lstUid = this.getNextEntryId();
const prefix = this.getPrefix(lstUid);
data[prefix + 'list'] = lst.list;
data[prefix + 'segment'] = lst.segment;
data[prefix + 'useSegmentation'] = !!lst.segment;
lsts.push(lstUid);
}
data[id] = lsts;
}
submitFormValuesMutator(data) {
const id = this.id;
const lsts = [];
for (const lstUid of data[id]) {
const prefix = this.getPrefix(lstUid);
const useSegmentation = data[prefix + 'useSegmentation'];
lsts.push({
list: data[prefix + 'list'],
segment: useSegmentation ? data[prefix + 'segment'] : null
});
}
data[id] = lsts;
for (const key in data) {
if (key.startsWith('data_') || key.startsWith(id + '_')) {
delete data[key];
}
}
}
populateFrom(data, lists) {
const id = this.id;
const lsts = [];
for (const lst of lists) {
const lstUid = this.getNextEntryId();
const prefix = this.getPrefix(lstUid);
data[prefix + 'list'] = lst.list;
data[prefix + 'segment'] = lst.segment;
data[prefix + 'useSegmentation'] = !!lst.segment;
lsts.push(lstUid);
}
data[id] = lsts;
}
localValidateFormValues(state) {
const id = this.id;
const t = this.t;
for (const lstUid of state.getIn([id, 'value'])) {
const prefix = this.getPrefix(lstUid);
if (!state.getIn([prefix + 'list', 'value'])) {
state.setIn([prefix + 'list', 'error'], t('listMustBeSelected'));
}
if (state.getIn([prefix + 'useSegmentation', 'value']) && !state.getIn([prefix + 'segment', 'value'])) {
state.setIn([prefix + 'segment', 'error'], t('segmentMustBeSelected'));
}
}
}
render() {
const t = this.t;
const owner = this.owner;
const id = this.id;
const listsColumns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('subscribers') },
{ data: 4, title: t('description') },
{ data: 5, title: t('namespace') }
];
const segmentsColumns = [
{ data: 1, title: t('name') }
];
const lstsEditEntries = [];
const lsts = owner.getFormValue(id) || [];
let lstOrderIdx = 0;
for (const lstUid of lsts) {
const prefix = this.getPrefix(lstUid);
const lstOrderIdxClosure = lstOrderIdx;
const selectedList = owner.getFormValue(prefix + 'list');
lstsEditEntries.push(
<div key={lstUid} className={campaignsStyles.entry + ' ' + campaignsStyles.entryWithButtons}>
<div className={campaignsStyles.entryButtons}>
{(this.allowEmpty || lsts.length > 1) &&
<Button
className="btn-secondary"
icon="trash-alt"
title={t('remove')}
onClickAsync={() => this.onRemoveListEntry(lstUid)}
/>
}
<Button
className="btn-secondary"
icon="plus"
title={t('insertNewEntryBeforeThisOne')}
onClickAsync={() => this.onAddListEntry(lstOrderIdxClosure)}
/>
{lstOrderIdx > 0 &&
<Button
className="btn-secondary"
icon="chevron-up"
title={t('moveUp')}
onClickAsync={() => this.onListEntryMoveUp(lstOrderIdxClosure)}
/>
}
{lstOrderIdx < lsts.length - 1 &&
<Button
className="btn-secondary"
icon="chevron-down"
title={t('moveDown')}
onClickAsync={() => this.onListEntryMoveDown(lstOrderIdxClosure)}
/>
}
</div>
<div className={campaignsStyles.entryContent}>
<TableSelect id={prefix + 'list'} label={t('list')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
<div>
<CheckBox id={prefix + 'useSegmentation'} label={t('segment')} text={t('useAParticularSegment')}/>
{selectedList && owner.getFormValue(prefix + 'useSegmentation') &&
<TableSelect id={prefix + 'segment'} withHeader dropdown dataUrl={`rest/segments-table/${selectedList}`} columns={segmentsColumns} selectionLabelIndex={1} />
}
</div>
</div>
</div>
);
lstOrderIdx += 1;
}
return (
<Fieldset label={t('lists')}>
{lstsEditEntries}
<div key="newEntry" className={campaignsStyles.newEntry}>
<Button
className="btn-secondary"
icon="plus"
label={t('addList')}
onClickAsync={() => this.onAddListEntry(lsts.length)}
/>
</div>
</Fieldset>
);
}
}

585
client/src/channels/CUD.js Normal file
View file

@ -0,0 +1,585 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {withTranslation} from '../lib/i18n';
import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page'
import {
AlignedRow,
Button,
ButtonRow,
CheckBox,
Dropdown,
Fieldset,
filterData,
Form,
FormSendMethod,
InputField,
StaticField,
TableSelect,
TextArea,
withForm,
withFormErrorHandlers
} from '../lib/form';
import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling';
import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {getTagLanguages, getTemplateTypes, getTypeForm, ResourceType} from '../templates/helpers';
import axios from '../lib/axios';
import styles from "../lib/styles.scss";
import campaignsStyles from "./styles.scss";
import {getUrl} from "../lib/urls";
import {campaignOverridables, CampaignSource, CampaignStatus} from "../../../shared/campaigns";
import moment from 'moment';
import {getMailerTypes} from "../send-configurations/helpers";
import {getCampaignLabels, ListsSelectorHelper} from "../campaigns/helpers";
import {withComponentMixins} from "../lib/decorator-helpers";
import interoperableErrors from "../../../shared/interoperable-errors";
import {Trans} from "react-i18next";
@withComponentMixins([
withTranslation,
withForm,
withErrorHandling,
withPageHelpers,
requiresAuthenticatedUser
])
export default class CUD extends Component {
constructor(props) {
super(props);
const t = props.t;
this.listsSelectorHelper = new ListsSelectorHelper(this, t, 'lists', true);
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN, true);
this.tagLanguages = getTagLanguages(props.t);
this.mailerTypes = getMailerTypes(props.t);
const { campaignTypeLabels } = getCampaignLabels(t);
this.campaignTypeLabels = campaignTypeLabels;
this.sourceLabels = {
[CampaignSource.CUSTOM]: t('customContent'),
[CampaignSource.CUSTOM_FROM_CAMPAIGN]: t('customContentClonedFromAnotherCampaign'),
[CampaignSource.TEMPLATE]: t('template'),
[CampaignSource.CUSTOM_FROM_TEMPLATE]: t('customContentClonedFromTemplate'),
[CampaignSource.URL]: t('url')
};
const sourceLabelsOrder = [
CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_CAMPAIGN , CampaignSource.TEMPLATE, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.URL
];
this.sourceOptions = [];
for (const key of sourceLabelsOrder) {
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.customTemplateTagLanguageOptions = [];
for (const key of mailtrainConfig.tagLanguages) {
this.customTemplateTagLanguageOptions.push({key, label: this.tagLanguages[key].name});
}
this.state = {
sendConfiguration: null
};
this.initForm({
onChange: {
send_configuration: ::this.onSendConfigurationChanged
},
onChangeBeforeValidation: ::this.onFormChangeBeforeValidation
});
}
static propTypes = {
action: PropTypes.string.isRequired,
entity: PropTypes.object,
permissions: PropTypes.object,
type: PropTypes.number
}
onFormChangeBeforeValidation(mutStateData, key, oldValue, newValue) {
let match;
if (key === 'data_sourceCustom_type') {
if (newValue) {
this.templateTypes[newValue].afterTypeChange(mutStateData);
}
}
if (key === 'data_sourceCustom_tag_language') {
if (newValue) {
const currentType = this.getFormValue('data_sourceCustom_type');
const isEdit = !!this.props.entity;
this.templateTypes[currentType].afterTagLanguageChange(mutStateData, isEdit);
}
}
this.listsSelectorHelper.onFormChangeBeforeValidation(mutStateData, key, oldValue, newValue);
}
onSendConfigurationChanged(newState, key, oldValue, sendConfigurationId) {
newState.sendConfiguration = null;
// noinspection JSIgnoredPromiseFromCall
this.fetchSendConfiguration(sendConfigurationId);
}
@withAsyncErrorHandler
async fetchSendConfiguration(sendConfigurationId) {
if (sendConfigurationId) {
this.fetchSendConfigurationId = sendConfigurationId;
try {
const result = await axios.get(getUrl(`rest/send-configurations-public/${sendConfigurationId}`));
if (sendConfigurationId === this.fetchSendConfigurationId) {
this.setState({
sendConfiguration: result.data
});
}
} catch (err) {
if (err instanceof interoperableErrors.PermissionDeniedError) {
this.setState({
sendConfiguration: null
});
} else {
throw err;
}
}
}
}
populateTemplateDefaults(data) {
// This is for CampaignSource.TEMPLATE and CampaignSource.CUSTOM_FROM_TEMPLATE
data.data_sourceTemplate = null;
// This is for CampaignSource.CUSTOM_FROM_CAMPAIGN
data.data_sourceCampaign = null;
// This is for CampaignSource.CUSTOM
data.data_sourceCustom_type = mailtrainConfig.editors[0];
data.data_sourceCustom_tag_language = mailtrainConfig.tagLanguages[0];
data.data_sourceCustom_data = {};
Object.assign(data, this.templateTypes[mailtrainConfig.editors[0]].initData());
// This is for CampaignSource.URL
data.data_sourceUrl = '';
}
getFormValuesMutator(data) {
this.populateTemplateDefaults(data);
if (data.source === CampaignSource.TEMPLATE || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
data.data_sourceTemplate = data.data.sourceTemplate;
} else if (data.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
data.data_sourceCampaign = data.data.sourceCampaign;
} else if (data.source === CampaignSource.CUSTOM) {
data.data_sourceCustom_type = data.data.sourceCustom.type;
data.data_sourceCustom_tag_language = data.data.sourceCustom.tag_language;
data.data_sourceCustom_data = data.data.sourceCustom.data;
this.templateTypes[data.data.sourceCustom.type].afterLoad(data);
} else if (data.source === CampaignSource.URL) {
data.data_sourceUrl = data.data.sourceUrl
}
for (const overridable of campaignOverridables) {
if (data[overridable + '_override'] === null) {
data[overridable + '_override'] = '';
data[overridable + '_overriden'] = false;
} else {
data[overridable + '_overriden'] = true;
}
}
this.listsSelectorHelper.getFormValuesMutator(data);
// noinspection JSIgnoredPromiseFromCall
this.fetchSendConfiguration(data.send_configuration);
}
submitFormValuesMutator(data) {
const isEdit = !!this.props.entity;
data.source = Number.parseInt(data.source);
data.data = {};
if (data.source === CampaignSource.TEMPLATE || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
data.data.sourceTemplate = data.data_sourceTemplate;
} else if (data.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
data.data.sourceCampaign = data.data_sourceCampaign;
} else if (data.source === CampaignSource.CUSTOM) {
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
data.data.sourceCustom = {
type: data.data_sourceCustom_type,
tag_language: data.data_sourceCustom_tag_language,
data: data.data_sourceCustom_data,
}
} else if (data.source === CampaignSource.URL) {
data.data.sourceUrl = data.data_sourceUrl;
}
for (const overridable of campaignOverridables) {
if (!data[overridable + '_overriden']) {
data[overridable + '_override'] = null;
}
delete data[overridable + '_overriden'];
}
this.listsSelectorHelper.submitFormValuesMutator(data);
return filterData(data, [
'name', 'description', 'namespace', 'cpg_name', 'cpg_description', 'send_configuration',
'subject', 'from_name_override', 'from_email_override', 'reply_to_override',
'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url',
'source', 'lists'
]);
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity);
} else {
const data = {};
for (const overridable of campaignOverridables) {
data[overridable + '_override'] = '';
data[overridable + '_overriden'] = false;
}
data.type = this.props.type;
data.name = '';
data.description = '';
data.cpg_name = '';
data.cpg_description = '';
this.listsSelectorHelper.populateFrom(data, []);
data.send_configuration = null;
data.namespace = getDefaultNamespace(this.props.permissions);
data.subject = '';
data.click_tracking_disabled = false;
data.open_tracking_disabled = false;
data.unsubscribe_url = '';
data.source = CampaignSource.CUSTOM;
this.populateTemplateDefaults(data);
this.populateFormValues(data);
}
}
localValidateFormValues(state) {
const t = this.props.t;
const isEdit = !!this.props.entity;
for (const key of state.keys()) {
state.setIn([key, 'error'], null);
}
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
}
const sourceTypeKey = Number.parseInt(state.getIn(['source', 'value']));
if (sourceTypeKey === CampaignSource.TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
if (!state.getIn(['data_sourceTemplate', 'value'])) {
state.setIn(['data_sourceTemplate', 'error'], t('templateMustBeSelected'));
}
} else if (sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
if (!state.getIn(['data_sourceCampaign', 'value'])) {
state.setIn(['data_sourceCampaign', 'error'], t('campaignMustBeSelected'));
}
} else if (sourceTypeKey === CampaignSource.CUSTOM) {
const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
if (!customTemplateTypeKey) {
state.setIn(['data_sourceCustom_type', 'error'], t('typeMustBeSelected'));
}
if (!state.getIn(['data_sourceCustom_tag_language', 'value'])) {
state.setIn(['data_sourceCustom_tag_language', 'error'], t('tagLanguageMustBeSelected'));
}
if (customTemplateTypeKey) {
this.templateTypes[customTemplateTypeKey].validate(state);
}
} else if (sourceTypeKey === CampaignSource.URL) {
if (!state.getIn(['data_sourceUrl', 'value'])) {
state.setIn(['data_sourceUrl', 'error'], t('urlMustNotBeEmpty'));
}
}
this.listsSelectorHelper.localValidateFormValues(state)
validateNamespace(t, state);
}
async save() {
await this.submitHandler();
}
@withFormErrorHandlers
async submitHandler(submitAndLeave) {
const t = this.props.t;
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `rest/channels/${this.props.entity.id}`;
} else {
sendMethod = FormSendMethod.POST;
url = 'rest/channels'
}
this.disableForm();
this.setFormStatusMessage('info', t('saving'));
const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url);
if (submitResult) {
if (this.props.entity) {
if (submitAndLeave) {
this.navigateToWithFlashMessage('/channels', 'success', t('Channel updated'));
} else {
await this.getFormValuesFromURL(`rest/channels/${this.props.entity.id}`);
this.enableForm();
this.setFormStatusMessage('success', t('Channel updated'));
}
} else {
if (submitAndLeave) {
this.navigateToWithFlashMessage('/channels', 'success', t('Channel created'));
} else {
this.navigateToWithFlashMessage(`/channels/${submitResult}/edit`, 'success', t('Channel created'));
}
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
}
}
render() {
const t = this.props.t;
const isEdit = !!this.props.entity;
const canModify = !isEdit || this.props.entity.permissions.includes('edit');
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
const sourceTypeKey = Number.parseInt(this.getFormValue('source'));
const campaignTypeKey = this.getFormValue('type');
const sendConfigurationsColumns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.mailerTypes[data].typeName },
{ data: 6, title: t('namespace') }
];
let sendSettings;
if (this.getFormValue('send_configuration')) {
if (this.state.sendConfiguration) {
sendSettings = [];
const addOverridable = (id, label) => {
if(this.state.sendConfiguration[id + '_overridable']){
if (this.getFormValue(id + '_overriden')) {
sendSettings.push(<InputField label={label} key={id + '_override'} id={id + '_override'}/>);
} else {
sendSettings.push(
<StaticField key={id + '_original'} label={label} id={id + '_original'} className={styles.formDisabled}>
{this.state.sendConfiguration[id]}
</StaticField>
);
}
sendSettings.push(<CheckBox key={id + '_overriden'} id={id + '_overriden'} text={t('override')} className={campaignsStyles.overrideCheckbox}/>);
}
else{
sendSettings.push(
<StaticField key={id + '_original'} label={label} id={id + '_original'} className={styles.formDisabled}>
{this.state.sendConfiguration[id]}
</StaticField>
);
}
};
addOverridable('from_name', t('fromName'));
addOverridable('from_email', t('fromEmailAddress'));
addOverridable('reply_to', t('replytoEmailAddress'));
} else {
sendSettings = <AlignedRow>{t('loadingSendConfiguration')}</AlignedRow>
}
} else {
sendSettings = null;
}
let templateEdit = null;
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
const templatesColumns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') },
];
let help = null;
if (sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
help = t('selectingATemplateCreatesACampaign');
}
// The "key" property here and in the TableSelect below is to tell React that these tables are different and should be rendered by different instances. Otherwise, React will use
// only one instance, which fails because Table does not handle updates in "columns" property
templateEdit = <TableSelect key="templateSelect" id="data_sourceTemplate" label={t('template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
} else if (sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
const campaignsColumns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.campaignTypeLabels[data] },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }
];
templateEdit = <TableSelect key="campaignSelect" id="data_sourceCampaign" label={t('campaign')} withHeader dropdown dataUrl='rest/campaigns-with-content-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('contentOfTheSelectedCampaignWillBeCopied')}/>;
} else if (sourceTypeKey === CampaignSource.CUSTOM) {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
let customTemplateTypeForm = null;
if (customTemplateTypeKey) {
customTemplateTypeForm = getTypeForm(this, customTemplateTypeKey, false);
}
templateEdit = <div>
<Dropdown id="data_sourceCustom_type" label={t('type')} options={this.customTemplateTypeOptions}/>
<Dropdown id="data_sourceCustom_tag_language" label={t('tagLanguage')} options={this.customTemplateTagLanguageOptions}/>
{customTemplateTypeForm}
</div>;
} else if (sourceTypeKey === CampaignSource.URL) {
templateEdit = <InputField id="data_sourceUrl" label={t('renderUrl')} help={t('ifAMessageIsSentThenThisUrlWillBePosTed')}/>
}
return (
<div>
{canDelete &&
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/channels/${this.props.entity.id}`}
backUrl={`/channels/${this.props.entity.id}/edit`}
successUrl="/channels"
deletingMsg={t('Deleting channel ...')}
deletedMsg={t('Channel deleted')}/>
}
<Title>{isEdit ? t('Edit Channel') : t('Create Channel')}</Title>
{!canModify &&
<div className="alert alert-warning" role="alert">
<Trans><b>Warning!</b> You do not have necessary permissions to edit this channel. Any changes that you perform here will be lost.</Trans>
</div>
}
{isEdit && this.props.entity.status === CampaignStatus.SENDING &&
<div className={`alert alert-info`} role="alert">
{t('formCannotBeEditedBecauseTheCampaignIs')}
</div>
}
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('name')}/>
{isEdit &&
<StaticField id="cid" className={styles.formDisabled} label={t('id')}>
{this.getFormValue('cid')}
</StaticField>
}
<TextArea id="description" label={t('description')}/>
<NamespaceSelect/>
<hr/>
<Fieldset label={t('Campaign defaults')}>
<InputField id="cpg_name" label={t('Campaign name')}/>
<TextArea id="cpg_description" label={t('Campaign description')}/>
</Fieldset>
<hr/>
{this.listsSelectorHelper.render()}
<hr/>
<Fieldset label={t('sendSettings')}>
<TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader withClear dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
{sendSettings}
<InputField label={t('subjectLine')} key="subject" id="subject"/>
<InputField id="unsubscribe_url" label={t('customUnsubscribeUrl')}/>
</Fieldset>
<hr/>
<Fieldset label={t('tracking')}>
<CheckBox id="open_tracking_disabled" text={t('disableOpenedTracking')}/>
<CheckBox id="click_tracking_disabled" text={t('disableClickedTracking')}/>
</Fieldset>
<hr/>
<Fieldset label={t('template')}>
<Dropdown id="source" label={t('contentSource')} options={this.sourceOptions}/>
</Fieldset>
{templateEdit}
<ButtonRow>
{canModify &&
<>
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
{isEdit && <Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(true)}/>}
</>
}
{canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/channels/${this.props.entity.id}/delete`}/> }
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -42,7 +42,7 @@ export default class List extends Component {
data: 1,
title: t('name'),
actions: data => {
const perms = data[10];
const perms = data[5];
if (perms.includes('view')) {
return [{label: data[1], link: `/channels/${data[0]}/campaigns`}];
} else {

View file

@ -4,7 +4,7 @@ import React from 'react';
import CampaignsList from '../campaigns/List';
import CampaignsCUD from '../campaigns/CUD';
import ChannelsList from './List';
//import ChannelsCUD from './CUD';
import ChannelsCUD from './CUD';
import Share from '../shares/Share';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"
import {namespaceCheckPermissions} from "../lib/namespace";
@ -29,7 +29,7 @@ function getMenus(t) {
panelRender: props => <ChannelsList permissions={props.permissions}/>,
children: {
':channelId([0-9]+)': {
title: resolved => t('channelName', {name: ellipsizeBreadcrumbLabel(resolved.channel.name)}),
title: resolved => t('Channel "{{name}}"', {name: ellipsizeBreadcrumbLabel(resolved.channel.name)}),
resolve: {
channel: params => `rest/channels/${params.channelId}`
},
@ -39,16 +39,14 @@ function getMenus(t) {
title: t('Campaigns'),
link: params => `/channels/${params.channelId}/campaigns`,
visible: resolved => resolved.channel.permissions.includes('view'),
panelRender: props => <CampaignsList channel={props.resolved.channel} />
panelRender: props => <CampaignsList channel={props.resolved.channel} permissions={props.permissions} />
},
/*
':action(edit|delete)': {
title: t('edit'),
link: params => `/channels/${params.channelId}/edit`,
visible: resolved => resolved.channel.permissions.includes('view') || resolved.channel.permissions.includes('edit'),
panelRender: props => <ChannelsCUD action={props.match.params.action} entity={props.resolved.channel} permissions={props.permissions} />
},
*/
share: {
title: t('share'),
link: params => `/channels/${params.channelId}/share`,
@ -61,7 +59,7 @@ function getMenus(t) {
title: t('createCampaign'),
link: params => `/channels/${params.channelId}/create`,
visible: resolved => resolved.channel.permissions.includes('createCampaign'),
panelRender: props => <CampaignsCUD action="create" channel={props.resolved.channel} permissions={props.permissions} />
panelRender: props => <CampaignsCUD action="create" createFromChannel={props.resolved.channel} permissions={props.permissions} />,
}
}
},

View file

@ -34,8 +34,6 @@ import styles from "./styles.scss";
import moment from "moment";
import {getUrl} from "./urls";
import {createComponentMixin, withComponentMixins} from "./decorator-helpers";
import cudStyles from "../../../mvis/ivis-core/client/src/settings/jobs/CUD.scss";
import {campaignOverridables, CampaignSource, CampaignType} from "../../../shared/campaigns";
const FormState = {
@ -931,207 +929,6 @@ class ButtonRow extends Component {
}
@withComponentMixins([
withTranslation,
withFormStateOwner
], null, ['submitFormValuesMutator', 'getFormValueIdForPicker'])
class ListCreator extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
className: PropTypes.string,
withOrder: PropTypes.bool,
};
constructor(props) {
super(props);
this._nextEntryId = 0;
}
componentDidMount() {
const values = this.props.initValues;
if (values && values.length > 0) {
this.getFormStateOwner().updateForm(mutState => {
let entryIds = mutState.getIn([this.props.id, 'value']);
if (!entryIds) {
entryIds = [];
}
for (const entryValue of values) {
const entryId = this.getNextEntryId();
mutState.setIn([this.getFormValueId(entryId), 'value'], entryValue);
entryIds.push(entryId);
}
mutState.setIn([this.props.id, 'value'], entryIds);
});
}
}
static getFormValuesMutator(pickerId, data) {
const elems = [];
for (const elem of data[pickerId]) {
const uid = this.getNextEntryId();
const prefix = this.getFormValueId(uid);
data[prefix + 'list'] = elem.list;
data[prefix + 'segment'] = elem.segment;
data[prefix + 'useSegmentation'] = !!elem.segment;
elems.push(uid);
}
data[pickerId] = elems;
}
static submitFormValuesMutator(pickerId, data) {
const entryValues = [];
const entryIds = data[pickerId];
if (!entryIds) {
return entryValues;
}
for (const entryId of entryIds) {
const entryFormId = ListCreator.getFormValueIdForPicker(pickerId, entryId);
const value = data[entryFormId];
entryValues.push(value);
delete data[entryFormId]
}
data[pickerId] = entryValues;
}
static getFormValueIdForPicker(pickerId, entryId) {
return `${pickerId}_${entryId}`;
}
getFormValueId(entryId) {
return ListCreator.getFormValueIdForPicker(this.props.id, entryId);
}
getNextEntryId() {
return this._nextEntryId++;
}
onAddListEntry(positionBefore) {
this.getFormStateOwner().updateForm(mutState => {
let entryIds = mutState.getIn([this.props.id, 'value']);
if (!entryIds) {
entryIds = [];
}
if (positionBefore == null) {
positionBefore = entryIds.length;
}
const entryId = this.getNextEntryId();
mutState.setIn([this.getFormValueId(entryId), 'value'], null);
mutState.setIn([this.props.id, 'value'], [...entryIds.slice(0, positionBefore), entryId, ...entryIds.slice(positionBefore)]);
});
}
onRemoveSetEntry(entryId) {
this.getFormStateOwner().updateForm(mutState => {
const entryIds = mutState.getIn([this.props.id, 'value']);
mutState.delete(this.getFormValueId(entryId));
mutState.setIn([this.props.id, 'value'], entryIds.filter(id => id !== entryId));
});
}
onListEntryMoveUp(position) {
const owner = this.getFormStateOwner();
const entryIds = owner.getFormValue(this.props.id);
owner.updateFormValue(this.props.id, [...entryIds.slice(0, position - 1), entryIds[position], entryIds[position - 1], ...entryIds.slice(position + 1)]);
}
onListEntryMoveDown(position) {
const owner = this.getFormStateOwner();
const entryIds = owner.getFormValue(this.props.id);
owner.updateFormValue(this.props.id, [...entryIds.slice(0, position), entryIds[position + 1], entryIds[position], ...entryIds.slice(position + 2)]);
}
render() {
const props = this.props;
const owner = this.getFormStateOwner();
const id = props.id;
const t = props.t;
const withOrder = props.withOrder;
const entries = [];
const entryIds = owner.getFormValue(id) || [];
const entryButtonsStyles = withOrder ? cudStyles.entryButtonsWithOrder : cudStyles.entryButtons;
for (let pos = 0; pos < entryIds.length; pos++) {
const entryId = entryIds[pos];
const elementId = this.getFormValueId(entryId);
entries.push(
<div key={entryId}
className={cudStyles.entry + (withOrder ? ' ' + cudStyles.withOrder : '') + ' ' + cudStyles.entryWithButtons}>
<div className={entryButtonsStyles}>
<Button
className="btn-secondary"
icon={`trash-alt ${withOrder ? "" : "fa-2x"}`}
title={t('remove')}
onClickAsync={() => this.onRemoveSetEntry(entryId)}
/>
{withOrder &&
<Button
className="btn-secondary"
icon="plus"
title={t('Insert new entry before this one')}
onClickAsync={() => this.onAddListEntry(pos)}
/>
}
{withOrder && pos > 0 &&
<Button
className="btn-secondary"
icon="chevron-up"
title={t('Move up')}
onClickAsync={() => this.onListEntryMoveUp(pos)}
/>
}
{withOrder && pos < entryIds.length - 1 &&
<Button
className="btn-secondary"
icon="chevron-down"
title={t('Move down')}
onClickAsync={() => this.onListEntryMoveDown(pos)}
/>
}
</div>
<div className={cudStyles.entryContent}>
{React.cloneElement(this.props.entryElement, {id: elementId})}
</div>
</div>
);
}
return (
<Fieldset id={id} className={props.classname} help={props.help} flat={props.flat} label={props.label}>
{entries}
<div key="newEntry" className={cudStyles.newEntry}>
<Button
className="btn-secondary"
icon="plus"
label={t('Add entry')}
onClickAsync={() => this.onAddListEntry(entryIds.length)}
/>
</div>
</Fieldset>
);
}
}
@withComponentMixins([
withFormStateOwner
])
@ -1200,6 +997,7 @@ class TableSelect extends Component {
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
format: PropTypes.string,
disabled: PropTypes.bool,
withClear: PropTypes.bool,
pageLength: PropTypes.number
}
@ -1245,6 +1043,15 @@ class TableSelect extends Component {
});
}
async clear() {
const owner = this.getFormStateOwner();
if (this.props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
owner.updateFormValue(this.props.id, null);
} else {
owner.updateFormValue(this.props.id, []);
}
}
refresh() {
this.table.refresh();
}
@ -1256,6 +1063,8 @@ class TableSelect extends Component {
const htmlId = 'form_' + id;
const t = props.t;
const selection = owner.getFormValue(id);
if (props.dropdown) {
const className = owner.addFormValidationClass('form-control', id);
@ -1271,6 +1080,7 @@ class TableSelect extends Component {
{!props.disabled &&
<div className="input-group-append">
<Button label={t('select')} className="btn-secondary" onClickAsync={::this.toggleOpen}/>
{props.withClear && selection && <Button icon="times" title={t('Clear')} className="btn-secondary" onClickAsync={::this.clear}/>}
</div>
}
</div>
@ -1285,7 +1095,7 @@ class TableSelect extends Component {
selectionAsArray={this.props.selectionAsArray}
withHeader={props.withHeader}
selectionKeyIndex={props.selectionKeyIndex}
selection={owner.getFormValue(id)}
selection={selection}
onSelectionDataAsync={::this.onSelectionDataAsync}
onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
</div>
@ -1305,7 +1115,7 @@ class TableSelect extends Component {
selectionAsArray={this.props.selectionAsArray}
withHeader={props.withHeader}
selectionKeyIndex={props.selectionKeyIndex}
selection={owner.getFormValue(id)}
selection={selection}
onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
</div>
</div>

View file

@ -211,7 +211,7 @@ function renderFrameWithContent(t, panelInFullScreen, showSidebar, primaryMenu,
</div>
<footer key="appFooter" className="app-footer">
<div className="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGitHub')}</a></div>
<div className="text-muted">&copy; 2020 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{t('sourceOnGitHub')}</a></div>
</footer>
</div>
);

View file

@ -39,7 +39,7 @@ export function getTagLanguages(t) {
};
}
export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEMPLATE) {
export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEMPLATE, allowEmpty = false) {
// The prefix is used to to enable use within other forms (i.e. campaign form)
const templateTypes = {};
@ -97,7 +97,9 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
dataUrl={`rest/mosaico-templates-by-tag-language-table/${tagLanguageKey}`}
columns={mosaicoTemplatesColumns}
selectionLabelIndex={1}
disabled={isEdit}/>
disabled={isEdit}
withClear={allowEmpty}
/>
} else {
return null;
}
@ -169,7 +171,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
},
validate: state => {
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
if (!mosaicoTemplate) {
if (!allowEmpty && !mosaicoTemplate) {
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('mosaicoTemplateMustBeSelected'));
}
}

View file

@ -298,7 +298,7 @@ defaultRoles:
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
customForm: [view, edit, delete, share]
channel: [view, edit, delete, share]
channel: [view, edit, delete, createCampaign, share]
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
report: [view, edit, delete, share, execute, viewContent, viewOutput]
@ -308,13 +308,13 @@ defaultRoles:
campaignsAdmin:
name: Campaigns Admin
description: In the respective namespace, the user has all permissions for managing lists, templates and campaigns and the permission to send to send configurations.
description: In the respective namespace, the user has all permissions for managing lists, channels, templates and campaigns and the permission to send to send configurations.
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createChannel, createCampaign]
children:
sendConfiguration: [viewPublic, sendWithoutOverrides, sendWithAllowedOverrides]
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
customForm: [view, edit, delete, share]
channel: [view, edit, delete, share]
channel: [view, edit, delete, createCampaign, share]
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
report: [view, edit, delete, share, execute, viewContent, viewOutput]
@ -325,7 +325,7 @@ defaultRoles:
campaignsCreator:
name: Campaigns Creator
description: In the respective namespace, the user has all permissions to create and manage templates and campaigns. The user can also read public data about send configurations and use Mosaico templates in the namespace.
permissions: [view, createTemplate, createChannel, createCampaign]
permissions: [view, createTemplate, createCampaign]
children:
sendConfiguration: [viewPublic]
channel: [view]

View file

@ -11,14 +11,14 @@ async function validateEntity(tx, entity) {
}
}
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
async function validateMoveTx(tx, context, entity, existing, entityTypeId, createOperation, deleteOperation) {
if (existing.namespace !== entity.namespace) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, createOperation);
await shares.enforceEntityPermissionTx(tx, context, entityTypeId, entity.id, deleteOperation);
}
}
module.exports = {
validateEntity,
validateMove
validateMoveTx
};

View file

@ -357,7 +357,7 @@ async function rawGetByTx(tx, key, id) {
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
.groupBy('campaigns.id')
.select([
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.channel', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject',
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url', 'campaigns.scheduled',
'campaigns.delivered', 'campaigns.unsubscribed', 'campaigns.bounced', 'campaigns.complained', 'campaigns.blacklisted', 'campaigns.opened', 'campaigns.clicks',
@ -412,6 +412,7 @@ async function getByIdTx(tx, context, id, withPermissions = true, content = Cont
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
entity = {
id: entity.id,
channel: entity.channel,
send_configuration: entity.send_configuration,
data: {
@ -454,6 +455,8 @@ async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
}
enforce(Number.isInteger(entity.source));
@ -481,6 +484,10 @@ async function _createTx(tx, context, entity, content) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
if (entity.channel) {
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.channel, 'createCampaign');
}
let copyFilesFrom = null;
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
copyFilesFrom = {
@ -570,6 +577,13 @@ async function createRssTx(tx, context, entity) {
return await _createTx(tx, context, entity, Content.RSS_ENTRY);
}
async function _validateChannelMoveTx(tx, context, entity, existing) {
if (existing.channel !== entity.channel) {
await shares.enforceEntityPermission(context, 'channel', entity.channel, 'createCampaign');
await shares.enforceEntityPermission(context, 'campaign', entity.id, 'delete');
}
}
async function updateWithConsistencyCheck(context, entity, content) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit');
@ -585,11 +599,13 @@ async function updateWithConsistencyCheck(context, entity, content) {
let filteredEntity = filterObject(entity, allowedKeysUpdate);
if (content === Content.ALL) {
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'campaign', 'createCampaign', 'delete');
await _validateChannelMoveTx(tx, context, entity, existing);
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
filteredEntity.data.sourceCustom = existing.data.sourceCustom;
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'campaign', 'createCampaign', 'delete');
await _validateChannelMoveTx(tx, context, entity, existing);
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
const data = existing.data;

View file

@ -16,8 +16,8 @@ const dependencyHelpers = require('../lib/dependency-helpers');
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const allowedKeys = ['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source'];
const allowedKeys = new Set(['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source']);
function hash(entity) {
@ -43,12 +43,27 @@ async function listDTAjax(context, params) {
);
}
async function _getByTx(tx, key, id, withPermissions = true) {
async function listWithCreateCampaignPermissionDTAjax(context, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'channel', requiredOperations: ['createCampaign'] }],
params,
builder => {
builder = builder.from('channels')
.innerJoin('namespaces', 'namespaces.id', 'channels.namespace');
return builder;
},
['channels.id', 'channels.name', 'channels.cid', 'channels.description', 'namespaces.name']
);
}
async function _getByTx(tx, context, key, id, withPermissions = true) {
const entity = await tx('channels').where('channels.' + key, id)
.leftJoin('channel_lists', 'channels.id', 'channel_lists.channel')
.groupBy('channels.id')
.select([
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.source',
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.cpg_name', 'channels.cpg_description', 'channels.source',
'channels.send_configuration', 'channels.from_name_override', 'channels.from_email_override', 'channels.reply_to_override', 'channels.subject',
'channels.data', 'channels.click_tracking_disabled', 'channels.open_tracking_disabled', 'channels.unsubscribe_url',
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', channel_lists.list, channel_lists.segment) ORDER BY channel_lists.id SEPARATOR \';\') as lists`)
@ -82,7 +97,7 @@ async function _getByTx(tx, key, id, withPermissions = true) {
async function getByIdTx(tx, context, id, withPermissions = true) {
await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'view');
return await _getByTx(tx, 'id', id, withPermissions);
return await _getByTx(tx, context, 'id', id, withPermissions);
}
async function getById(context, id, withPermissions = true) {
@ -97,12 +112,13 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
if (entity.source !== null) {
enforce(Number.isInteger(entity.source));
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
} else if (entity.source === CampaignSource.CUSTOM) {
enforce(allTagLanguages.includes(entity.data.sourceCustom.tag_language), `Invalid tag language '${entity.data.sourceCustom.tag_language}'`);
} else if (entity.source === CampaignSource.URL) {
} else {
enforce(false, 'Unknown channel source');
}
@ -156,7 +172,7 @@ async function updateWithConsistencyCheck(context, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.id, 'edit');
const existing = await _getByTx(tx, 'id', entity.id, false);
const existing = await _getByTx(tx, context, 'id', entity.id, false);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
@ -166,7 +182,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, context, entity, false);
let filteredEntity = filterObject(entity, allowedKeys);
await namespaceHelpers.validateMove(context, entity, existing, 'channel', 'createCampaign', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'channel', 'createCampaign', 'delete');
await tx('channel_lists').where('channel', entity.id).del();
await tx('channel_lists').insert(entity.lists.map(x => ({channel: entity.id, ...x})));
@ -197,8 +213,8 @@ async function remove(context, id) {
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listWithCreateCampaignPermissionDTAjax = listWithCreateCampaignPermissionDTAjax;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;
module.exports.create = create;

View file

@ -169,7 +169,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'customForm', 'createCustomForm', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'customForm', 'createCustomForm', 'delete');
const form = filterObject(entity, allowedFormKeys);
enforce(!Object.keys(checkForMjmlErrors(form)).length, 'Error(s) in form templates');

View file

@ -263,7 +263,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'list', 'createList', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'list', 'createList', 'delete');
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -89,7 +89,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
await tx('mosaico_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -198,7 +198,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
// namespaceHelpers.validateEntity is not needed here because it is part of the tree traversal check below
await namespaceHelpers.validateMove(context, entity, existing, 'namespace', 'createNamespace', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'namespace', 'createNamespace', 'delete');
let iter = entity;
while (iter.namespace != null) {

View file

@ -66,7 +66,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'reportTemplate', 'createReportTemplate', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'reportTemplate', 'createReportTemplate', 'delete');
await tx('report_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -102,7 +102,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'report', 'createReport', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'report', 'createReport', 'delete');
entity.params = JSON.stringify(entity.params);

View file

@ -155,7 +155,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'sendConfiguration', 'createSendConfiguration', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'sendConfiguration', 'createSendConfiguration', 'delete');
await tx('send_configurations').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -132,7 +132,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
entity.data = JSON.stringify(entity.data);
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'template', 'createTemplate', 'delete');
const filteredEntity = filterObject(entity, allowedKeys);

View file

@ -11,6 +11,10 @@ router.postAsync('/channels-table', passport.loggedIn, async (req, res) => {
return res.json(await channels.listDTAjax(req.context, req.body));
});
router.postAsync('/channels-with-create-campaign-permission-table', passport.loggedIn, async (req, res) => {
return res.json(await channels.listWithCreateCampaignPermissionDTAjax(req.context, req.body));
});
router.getAsync('/channels/:channelId', passport.loggedIn, async (req, res) => {
const channel = await channels.getById(req.context, castToInteger(req.params.channelId), true);
channel.hash = channels.hash(channel);
@ -25,7 +29,7 @@ router.putAsync('/channels/:channelId', passport.loggedIn, passport.csrfProtecti
const entity = req.body;
entity.id = castToInteger(req.params.channelId);
await channels.updateWithConsistencyCheck(req.context);
await channels.updateWithConsistencyCheck(req.context, entity);
return res.json();
});

View file

@ -14,7 +14,7 @@
"node": ">=10.0.0"
},
"dependencies": {
"zone-mta": "^2.2.1",
"zone-mta": "^2.3.0",
"zonemta-delivery-counters": "^1.0.1",
"zonemta-limiter": "^1.0.0",
"zonemta-loop-breaker": "^1.0.2"