Campaign UI and model adjusted to allow sending a campaign to multiple lists
This commit is contained in:
parent
130c953d94
commit
67d7129f7b
16 changed files with 334 additions and 78 deletions
|
@ -15,6 +15,7 @@ import {
|
|||
ButtonRow,
|
||||
CheckBox,
|
||||
Dropdown,
|
||||
Fieldset,
|
||||
Form,
|
||||
FormSendMethod,
|
||||
InputField,
|
||||
|
@ -39,6 +40,7 @@ import {
|
|||
} 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,
|
||||
|
@ -100,6 +102,8 @@ export default class CUD extends Component {
|
|||
sendConfiguration: null
|
||||
};
|
||||
|
||||
this.nextListEntryId = 0;
|
||||
|
||||
this.initForm({
|
||||
onChange: {
|
||||
send_configuration: ::this.onSendConfigurationChanged
|
||||
|
@ -116,6 +120,12 @@ export default class CUD extends Component {
|
|||
type: PropTypes.number
|
||||
}
|
||||
|
||||
getNextListEntryId() {
|
||||
const id = this.nextListEntryId;
|
||||
this.nextListEntryId += 1;
|
||||
return id;
|
||||
}
|
||||
|
||||
onCustomTemplateTypeChanged(mutState, key, oldType, type) {
|
||||
if (type) {
|
||||
this.templateTypes[type].afterTypeChange(mutState);
|
||||
|
@ -156,12 +166,24 @@ export default class CUD extends Component {
|
|||
data.data_feedUrl = data.data.feedUrl;
|
||||
}
|
||||
|
||||
data.useSegmentation = !!data.segment;
|
||||
|
||||
for (const overridable of campaignOverridables) {
|
||||
data[overridable + '_overriden'] = !!data[overridable + '_override'];
|
||||
}
|
||||
|
||||
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.fetchSendConfiguration(data.send_configuration);
|
||||
});
|
||||
|
||||
|
@ -172,6 +194,9 @@ export default class CUD extends Component {
|
|||
data[overridable + '_overriden'] = false;
|
||||
}
|
||||
|
||||
const lstUid = this.getNextListEntryId();
|
||||
const lstPrefix = 'lists_' + lstUid + '_';
|
||||
|
||||
this.populateFormValues({
|
||||
...data,
|
||||
|
||||
|
@ -179,9 +204,12 @@ export default class CUD extends Component {
|
|||
|
||||
name: '',
|
||||
description: '',
|
||||
list: null,
|
||||
segment: null,
|
||||
useSegmentation: false,
|
||||
|
||||
[lstPrefix + 'list']: null,
|
||||
[lstPrefix + 'segment']: null,
|
||||
[lstPrefix + 'useSegmentation']: false,
|
||||
lists: [lstUid],
|
||||
|
||||
send_configuration: null,
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
|
||||
|
@ -227,14 +255,6 @@ export default class CUD extends Component {
|
|||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||
}
|
||||
|
||||
if (!state.getIn(['list', 'value'])) {
|
||||
state.setIn(['list', 'error'], t('List must be selected'));
|
||||
}
|
||||
|
||||
if (state.getIn(['useSegmentation', 'value']) && !state.getIn(['segment', 'value'])) {
|
||||
state.setIn(['segment', 'error'], t('Segment must be selected'));
|
||||
}
|
||||
|
||||
if (!state.getIn(['send_configuration', 'value'])) {
|
||||
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
|
||||
}
|
||||
|
@ -281,10 +301,25 @@ 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('List must be selected'));
|
||||
}
|
||||
|
||||
if (campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) {
|
||||
if (state.getIn([prefix + 'useSegmentation', 'value']) && !state.getIn([prefix + 'segment', 'value'])) {
|
||||
state.setIn([prefix + 'segment', 'error'], t('Segment must be selected'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateNamespace(t, state);
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const isEdit = !!this.props.entity;
|
||||
const t = this.props.t;
|
||||
|
||||
let sendMethod, url;
|
||||
|
@ -302,11 +337,6 @@ export default class CUD extends Component {
|
|||
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
data.source = Number.parseInt(data.source);
|
||||
|
||||
if (!data.useSegmentation) {
|
||||
data.segment = null;
|
||||
}
|
||||
delete data.useSegmentation;
|
||||
|
||||
data.data = {};
|
||||
if (data.source === CampaignSource.TEMPLATE || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
data.data.sourceTemplate = data.data_sourceTemplate;
|
||||
|
@ -316,7 +346,7 @@ export default class CUD extends Component {
|
|||
data.data.sourceCampaign = data.data_sourceCampaign;
|
||||
}
|
||||
|
||||
if (data.source === CampaignSource.CUSTOM) {
|
||||
if (!isEdit && data.source === CampaignSource.CUSTOM) {
|
||||
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
|
||||
|
||||
data.data.sourceCustom = {
|
||||
|
@ -342,8 +372,21 @@ 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'] && (data.type === CampaignType.REGULAR || data.type === CampaignType.RSS);
|
||||
|
||||
lsts.push({
|
||||
list: data[prefix + 'list'],
|
||||
segment: useSegmentation ? data[prefix + 'segment'] : null
|
||||
});
|
||||
}
|
||||
data.lists = lsts;
|
||||
|
||||
for (const key in data) {
|
||||
if (key.startsWith('data_')) {
|
||||
if (key.startsWith('data_') || key.startsWith('lists_')) {
|
||||
delete data[key];
|
||||
}
|
||||
}
|
||||
|
@ -364,6 +407,47 @@ 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;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
@ -390,6 +474,81 @@ export default class CUD extends Component {
|
|||
{ 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-default"
|
||||
icon="remove"
|
||||
title={t('Remove')}
|
||||
onClickAsync={() => this.onRemoveListEntry(lstUid)}
|
||||
/>
|
||||
}
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="plus"
|
||||
title={t('Insert new entry before this one')}
|
||||
onClickAsync={() => this.onAddListEntry(lstOrderIdxClosure)}
|
||||
/>
|
||||
{lstOrderIdx > 0 &&
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="chevron-up"
|
||||
title={t('Move up')}
|
||||
onClickAsync={() => this.onListEntryMoveUp(lstOrderIdxClosure)}
|
||||
/>
|
||||
}
|
||||
{lstOrderIdx < lsts.length - 1 &&
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="chevron-down"
|
||||
title={t('Move down')}
|
||||
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} />
|
||||
|
||||
{(campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) &&
|
||||
<div>
|
||||
<CheckBox id={prefix + 'useSegmentation'} label={t('Segment')} text={t('Use a particular segment')}/>
|
||||
{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-default"
|
||||
icon="plus"
|
||||
label={t('Add list')}
|
||||
onClickAsync={() => this.onAddListEntry(lsts.length)}
|
||||
/>
|
||||
</div>
|
||||
</Fieldset>;
|
||||
|
||||
|
||||
const sendConfigurationsColumns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
|
@ -495,6 +654,8 @@ export default class CUD extends Component {
|
|||
saveButtonLabel = t('Save and edit campaign');
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canDelete &&
|
||||
|
@ -520,12 +681,7 @@ export default class CUD extends Component {
|
|||
|
||||
<hr/>
|
||||
|
||||
<TableSelect id="list" label={t('List')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
|
||||
|
||||
<CheckBox id="useSegmentation" label={t('Segment')} text={t('Use a particular segment')}/>
|
||||
{this.getFormValue('useSegmentation') &&
|
||||
<TableSelect id="segment" withHeader dropdown dataUrl={`rest/segments-table/${this.getFormValue('list')}`} columns={segmentsColumns} selectionLabelIndex={1} />
|
||||
}
|
||||
{lstsEdit}
|
||||
|
||||
<hr/>
|
||||
|
||||
|
|
38
client/src/campaigns/styles.scss
Normal file
38
client/src/campaigns/styles.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
.entry {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-bottom: 15px;
|
||||
min-height: 91px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0px none;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.entryButtons {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 19px;
|
||||
|
||||
button {
|
||||
padding: 2px 3px;
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.entryWithButtons > .entryContent {
|
||||
margin-right: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.newEntry{
|
||||
text-align: right;
|
||||
margin-bottom: 15px;
|
||||
}
|
|
@ -191,6 +191,8 @@ export default class CUD extends Component {
|
|||
{ data: 5, title: t('Namespace') }
|
||||
];
|
||||
|
||||
const campaignLists = this.props.campaign.lists.map(x => x.list).join(';');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isEdit &&
|
||||
|
@ -221,7 +223,7 @@ export default class CUD extends Component {
|
|||
{entityKey === Entity.CAMPAIGN && <Dropdown id="campaignEvent" label={t('Event')} options={this.eventOptions[Entity.CAMPAIGN]} help={t('Select the event that triggers sending the campaign.')}/>}
|
||||
|
||||
{entityKey === Entity.CAMPAIGN &&
|
||||
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${this.props.campaign.list}`} columns={campaignsColumns} selectionLabelIndex={1} />
|
||||
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${campaignLists}`} columns={campaignsColumns} selectionLabelIndex={1} />
|
||||
}
|
||||
|
||||
<CheckBox id="enabled" text={t('Enabled')}/>
|
||||
|
|
|
@ -44,11 +44,10 @@ export default class List extends Component {
|
|||
const columns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('List') },
|
||||
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
|
||||
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
|
||||
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
|
||||
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
|
||||
{ data: 3, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
|
||||
{ data: 4, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[3]][data], searchable: false },
|
||||
{ data: 5, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
|
||||
{ data: 6, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
|
||||
{
|
||||
actions: data => {
|
||||
const actions = [];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue