- "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'));
}
}