'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, Form, FormSendMethod, InputField, StaticField, TableSelect, TextArea, withForm } from '../lib/form'; import { withAsyncErrorHandler, withErrorHandling } from '../lib/error-handling'; import { NamespaceSelect, validateNamespace } from '../lib/namespace'; import {DeleteModalDialog} from "../lib/modals"; import mailtrainConfig from 'mailtrainConfig'; import { 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, CampaignType } from "../../../shared/campaigns"; import moment from 'moment'; import {getMailerTypes} from "../send-configurations/helpers"; import {getCampaignLabels} from "./helpers"; import {withComponentMixins} from "../lib/decorator-helpers"; @withComponentMixins([ withTranslation, withForm, withErrorHandling, withPageHelpers, requiresAuthenticatedUser ]) export default class CUD extends Component { constructor(props) { super(props); const t = props.t; this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN); this.mailerTypes = getMailerTypes(props.t); const { campaignTypeLabels } = getCampaignLabels(t); this.campaignTypeLabels = campaignTypeLabels; this.createTitles = { [CampaignType.REGULAR]: t('createRegularCampaign'), [CampaignType.RSS]: t('createRssCampaign'), [CampaignType.TRIGGERED]: t('createTriggeredCampaign'), }; this.editTitles = { [CampaignType.REGULAR]: t('editRegularCampaign'), [CampaignType.RSS]: t('editRssCampaign'), [CampaignType.TRIGGERED]: t('editTriggeredCampaign'), }; this.sourceLabels = { [CampaignSource.TEMPLATE]: t('template'), [CampaignSource.CUSTOM_FROM_TEMPLATE]: t('customContentClonedFromTemplate'), [CampaignSource.CUSTOM_FROM_CAMPAIGN]: t('customContentClonedFromAnotherCampaign'), [CampaignSource.CUSTOM]: t('customContent'), [CampaignSource.URL]: t('url') }; this.sourceOptions = []; for (const key in this.sourceLabels) { this.sourceOptions.push({key, label: this.sourceLabels[key]}); } this.customTemplateTypeOptions = []; for (const key of mailtrainConfig.editors) { this.customTemplateTypeOptions.push({key, label: this.templateTypes[key].typeName}); } this.state = { sendConfiguration: null }; this.nextListEntryId = 0; this.initForm({ onChange: { send_configuration: ::this.onSendConfigurationChanged }, onChangeBeforeValidation: { data_sourceCustom_type: ::this.onCustomTemplateTypeChanged } }); } static propTypes = { action: PropTypes.string.isRequired, entity: PropTypes.object, type: PropTypes.number } getNextListEntryId() { const id = this.nextListEntryId; this.nextListEntryId += 1; return id; } onCustomTemplateTypeChanged(mutStateData, key, oldType, type) { if (type) { this.templateTypes[type].afterTypeChange(mutStateData); } } onSendConfigurationChanged(newState, key, oldValue, sendConfigurationId) { newState.sendConfiguration = null; // noinspection JSIgnoredPromiseFromCall this.fetchSendConfiguration(sendConfigurationId); } @withAsyncErrorHandler async fetchSendConfiguration(sendConfigurationId) { if (sendConfigurationId) { this.fetchSendConfigurationId = sendConfigurationId; const result = await axios.get(getUrl(`rest/send-configurations-public/${sendConfigurationId}`)); if (sendConfigurationId === this.fetchSendConfigurationId) { this.setState({ sendConfiguration: result.data }); } } } getFormValuesMutator(data) { // The source cannot be changed once campaign is created. Thus we don't have to initialize fields for all other sources if (data.source === CampaignSource.TEMPLATE) { data.data_sourceTemplate = data.data.sourceTemplate; } if (data.source === CampaignSource.URL) { data.data_sourceUrl = data.data.sourceUrl; } if (data.type === CampaignType.RSS) { data.data_feedUrl = data.data.feedUrl; } for (const overridable of campaignOverridables) { data[overridable + '_overriden'] = data[overridable + '_override'] !== null; } 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; // noinspection JSIgnoredPromiseFromCall this.fetchSendConfiguration(data.send_configuration); } componentDidMount() { if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity, ::this.getFormValuesMutator); if (this.props.entity.status === CampaignStatus.SENDING) { this.disableForm(); } } else { const data = {}; for (const overridable of campaignOverridables) { data[overridable + '_override'] = ''; data[overridable + '_overriden'] = false; } const lstUid = this.getNextListEntryId(); const lstPrefix = 'lists_' + lstUid + '_'; this.populateFormValues({ ...data, type: this.props.type, name: '', description: '', [lstPrefix + 'list']: null, [lstPrefix + 'segment']: null, [lstPrefix + 'useSegmentation']: false, lists: [lstUid], send_configuration: null, namespace: mailtrainConfig.user.namespace, click_tracking_disabled: false, open_tracking_disabled: false, unsubscribe_url: '', source: CampaignSource.TEMPLATE, // This is for CampaignSource.TEMPLATE and CampaignSource.CUSTOM_FROM_TEMPLATE data_sourceTemplate: null, // This is for CampaignSource.CUSTOM_FROM_CAMPAIGN data_sourceCampaign: null, // This is for CampaignSource.CUSTOM data_sourceCustom_type: mailtrainConfig.editors[0], data_sourceCustom_data: {}, data_sourceCustom_html: '', data_sourceCustom_text: '', ...this.templateTypes[mailtrainConfig.editors[0]].initData(), // This is for CampaignSource.URL data_sourceUrl: '', // This is for CampaignType.RSS data_feedUrl: '' }); } } 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')); } if (!state.getIn(['send_configuration', 'value'])) { state.setIn(['send_configuration', 'error'], t('sendConfigurationMustBeSelected')); } if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) { state.setIn(['from_email_override', 'error'], t('fromEmailMustNotBeEmpty')); } const campaignTypeKey = state.getIn(['type', 'value']); const sourceTypeKey = Number.parseInt(state.getIn(['source', 'value'])); if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) { if (!state.getIn(['data_sourceTemplate', 'value'])) { state.setIn(['data_sourceTemplate', 'error'], t('templateMustBeSelected')); } } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { if (!state.getIn(['data_sourceCampaign', 'value'])) { state.setIn(['data_sourceCampaign', 'error'], t('campaignMustBeSelected')); } } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) { // The type is used only in create form. In case of CUSTOM_FROM_TEMPLATE or CUSTOM_FROM_CAMPAIGN, it is determined by the source template, so no need to check it here const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']); if (!customTemplateTypeKey) { state.setIn(['data_sourceCustom_type', 'error'], t('typeMustBeSelected')); } 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')); } } if (campaignTypeKey === CampaignType.RSS) { if (!state.getIn(['data_feedUrl', 'value'])) { state.setIn(['data_feedUrl', 'error'], t('rssFeedUrlMustBeGiven')); } } 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 (campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) { if (state.getIn([prefix + 'useSegmentation', 'value']) && !state.getIn([prefix + 'segment', 'value'])) { state.setIn([prefix + 'segment', 'error'], t('segmentMustBeSelected')); } } } validateNamespace(t, state); } static AfterSubmitAction = { STAY: 0, LEAVE: 1, STATUS: 2 } async submitHandler(afterSubmitAction) { const isEdit = !!this.props.entity; const t = this.props.t; let sendMethod, url; if (this.props.entity) { sendMethod = FormSendMethod.PUT; url = `rest/campaigns-settings/${this.props.entity.id}`; } else { sendMethod = FormSendMethod.POST; url = 'rest/campaigns' } this.disableForm(); this.setFormStatusMessage('info', t('saving')); const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { 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; } if (data.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) { data.data.sourceCampaign = data.data_sourceCampaign; } if (!isEdit && data.source === CampaignSource.CUSTOM) { this.templateTypes[data.data_sourceCustom_type].beforeSave(data); data.data.sourceCustom = { type: data.data_sourceCustom_type, data: data.data_sourceCustom_data, html: data.data_sourceCustom_html, text: data.data_sourceCustom_text } } if (data.source === CampaignSource.URL) { data.data.sourceUrl = data.data_sourceUrl; } if (data.type === CampaignType.RSS) { data.data.feedUrl = data.data_feedUrl; } for (const overridable of campaignOverridables) { if (!data[overridable + '_overriden']) { data[overridable + '_override'] = null; } 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_') || key.startsWith('lists_')) { delete data[key]; } } }); if (submitResult) { if (this.props.entity) { if (afterSubmitAction === CUD.AfterSubmitAction.STATUS) { this.navigateToWithFlashMessage(`/campaigns/${this.props.entity.id}/status`, 'success', t('Campaign updated')); } else if (afterSubmitAction === CUD.AfterSubmitAction.LEAVE) { this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign updated')); } else { await this.getFormValuesFromURL(`rest/campaigns-settings/${this.props.entity.id}`, ::this.getFormValuesMutator); this.enableForm(); this.setFormStatusMessage('success', t('Campaign updated')); } } else { const sourceTypeKey = Number.parseInt(this.getFormValue('source')); if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { this.navigateToWithFlashMessage(`/campaigns/${submitResult}/content`, 'success', t('Campaign created')); } else { if (afterSubmitAction === CUD.AfterSubmitAction.STATUS) { this.navigateToWithFlashMessage(`/campaigns/${submitResult}/status`, 'success', t('Campaign created')); } else if (afterSubmitAction === CUD.AfterSubmitAction.LEAVE) { this.navigateToWithFlashMessage(`/campaigns`, 'success', t('Campaign created')); } else { this.navigateToWithFlashMessage(`/campaigns/${submitResult}/edit`, 'success', t('Campaign created')); } } } } else { this.enableForm(); this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd')); } } 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; const canDelete = isEdit && this.props.entity.permissions.includes('delete'); let extraSettings = null; const sourceTypeKey = Number.parseInt(this.getFormValue('source')); const campaignTypeKey = this.getFormValue('type'); if (campaignTypeKey === CampaignType.RSS) { extraSettings = } const listsColumns = [ { data: 1, title: t('name') }, { data: 2, title: t('id'), render: data => {data} }, { 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 = 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(
{lsts.length > 1 &&
{(campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) &&
{selectedList && this.getFormValue(prefix + 'useSegmentation') && }
}
); lstOrderIdx += 1; } const lstsEdit =
{lstsEditEntries}
; const sendConfigurationsColumns = [ { data: 1, title: t('name') }, { data: 2, title: t('id'), render: data => {data} }, { data: 3, title: t('description') }, { data: 4, title: t('type'), render: data => this.mailerTypes[data].typeName }, { data: 5, title: t('created'), render: data => moment(data).fromNow() }, { data: 6, title: t('namespace') } ]; let sendSettings; if (this.getFormValue('send_configuration')) { if (this.state.sendConfiguration) { sendSettings = []; const addOverridable = (id, label) => { sendSettings.push(); if (this.getFormValue(id + '_overriden')) { sendSettings.push(); } else { sendSettings.push( {this.state.sendConfiguration[id]} ); } }; addOverridable('from_name', t('fromName')); addOverridable('from_email', t('fromEmailAddress')); addOverridable('reply_to', t('replytoEmailAddress')); addOverridable('subject', t('subjectLine')); } else { sendSettings = {t('loadingSendConfiguration')} } } else { sendSettings = null; } let sourceEdit = null; if (isEdit) { if (!(sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN)) { sourceEdit = {this.sourceLabels[sourceTypeKey]}; } } else { sourceEdit = } 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: 4, title: t('created'), render: data => moment(data).fromNow() }, { data: 5, 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 = ; } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { const campaignsColumns = [ { data: 1, title: t('name') }, { data: 2, title: t('id'), render: data => {data} }, { 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 = ; } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) { const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type'); let customTemplateTypeForm = null; if (customTemplateTypeKey) { customTemplateTypeForm = getTypeForm(this, customTemplateTypeKey, isEdit); } templateEdit =
{customTemplateTypeForm}
; } else if (sourceTypeKey === CampaignSource.URL) { templateEdit = } return (
{canDelete && } {isEdit ? this.editTitles[this.getFormValue('type')] : this.createTitles[this.getFormValue('type')]} {isEdit && this.props.entity.status === CampaignStatus.SENDING &&
{t('formCannotBeEditedBecauseTheCampaignIs')}
}
{isEdit && {this.getFormValue('cid')} }