Editing of campaigns seems to work

This commit is contained in:
Tomas Bures 2018-08-03 17:05:55 +05:30
parent b1c667d13d
commit 7b46c4b4b0
27 changed files with 335 additions and 130 deletions

View file

@ -217,6 +217,7 @@ export default class CUD extends Component {
localValidateFormValues(state) {
const t = this.props.t;
const isEdit = !!this.props.entity;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
@ -259,17 +260,17 @@ export default class CUD extends Component {
}
}
if (sourceTypeKey === CampaignSource.TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
if (!state.getIn(['data_sourceTemplate', 'value'])) {
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
}
} else if (sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
if (!state.getIn(['data_sourceCampaign', 'value'])) {
state.setIn(['data_sourceCampaign', 'error'], t('Campaign must be selected'));
}
} else if (sourceTypeKey === CampaignSource.CUSTOM) {
} 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) {
@ -302,7 +303,7 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `rest/campaigns/${this.props.entity.id}`
url = `rest/campaigns-settings/${this.props.entity.id}`;
} else {
sendMethod = FormSendMethod.POST;
url = 'rest/campaigns'
@ -312,6 +313,8 @@ export default class CUD extends Component {
this.setFormStatusMessage('info', t('Saving ...'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.source = Number.parseInt(data.source);
if (!data.useSegmentation) {
data.segment = null;
}
@ -460,18 +463,20 @@ export default class CUD extends Component {
help = t('Selecting a template creates a campaign specific copy from it.');
}
templateEdit = <TableSelect id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
// 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 (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
const campaignsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypes[data] },
{ data: 7, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 8, title: t('Namespace') }
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') }
];
templateEdit = <TableSelect id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>;
templateEdit = <TableSelect key="campaignSelect" id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-with-content-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>;
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');

View file

@ -88,14 +88,8 @@ export default class CustomContent extends Component {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `rest/campaigns/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = 'rest/campaigns'
}
const sendMethod = FormSendMethod.PUT;
const url = `rest/campaigns-content/${this.props.entity.id}`;
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));

View file

@ -20,7 +20,7 @@ function getMenus(t) {
':campaignId([0-9]+)': {
title: resolved => t('Campaign "{{name}}"', {name: resolved.campaign.name}),
resolve: {
campaign: params => `rest/campaigns/${params.campaignId}`
campaign: params => `rest/campaigns-settings/${params.campaignId}`
},
link: params => `/campaigns/${params.campaignId}/edit`,
navs: {
@ -33,20 +33,23 @@ function getMenus(t) {
content: {
title: t('Content'),
link: params => `/campaigns/${params.campaignId}/content`,
resolve: {
campaignContent: params => `rest/campaigns-content/${params.campaignId}`
},
visible: resolved => resolved.campaign.permissions.includes('edit') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
panelRender: props => <Content entity={props.resolved.campaign} />
panelRender: props => <Content entity={props.resolved.campaignContent} />
},
files: {
title: t('Files'),
link: params => `/campaigns/${params.campaignId}/files`,
visible: resolved => resolved.campaign.permissions.includes('viewFiles') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
panelRender: props => <Files title={t('Files')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="file" managePermission="manageFiles"/>
panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the content of the campaign.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="file" managePermission="manageFiles"/>
},
attachments: {
title: t('Attachments'),
link: params => `/campaigns/${params.campaignId}/attachments`,
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
panelRender: props => <Files title={t('Attachments')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to resulting eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
},
share: {
title: t('Share'),

View file

@ -3,7 +3,10 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import {requiresAuthenticatedUser} from "./page";
import {
requiresAuthenticatedUser,
Title
} from "./page";
import {withErrorHandling} from "./error-handling";
import {Table} from "./table";
import Dropzone from "react-dropzone";
@ -32,6 +35,7 @@ export default class Files extends Component {
static propTypes = {
title: PropTypes.string,
help: PropTypes.string,
entity: PropTypes.object.isRequired,
entityTypeId: PropTypes.string.isRequired,
entitySubTypeId: PropTypes.string.isRequired,
@ -151,6 +155,10 @@ export default class Files extends Component {
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
</ModalDialog>
{this.props.title && <Title>{this.props.title}</Title>}
{this.props.help && <p>{this.props.help}</p>}
{
this.props.entity.permissions.includes(this.props.managePermission) &&
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">

View file

@ -147,8 +147,8 @@ export class MosaicoSandbox extends Component {
});
const config = {
imgProcessorBackend: getTrustedUrl(`mosaico/img/${this.props.entityTypeId}/${this.props.entityId}`),
emailProcessorBackend: getSandboxUrl('mosaico/dl/'),
imgProcessorBackend: getTrustedUrl('mosaico/img'),
emailProcessorBackend: getSandboxUrl('mosaico/dl'),
fileuploadConfig: {
url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
},

View file

@ -20,6 +20,11 @@
:global .ace_editor {
border: 1px solid #ccc;
}
.buttonRow:last-child {
// This is to move Save/Delete buttons a bit down
margin-top: 15px;
}
}
.dayPickerWrapper {
@ -27,7 +32,6 @@
}
.buttonRow {
margin-top: 15px;
}
.buttonRow > * {

View file

@ -78,13 +78,20 @@ export default class List extends Component {
});
}
if (perms.includes('manageFields')) {
if (perms.includes('viewFields')) {
actions.push({
label: <Icon icon="th-list" title={t('Manage Fields')}/>,
label: <Icon icon="th-list" title={t('Fields')}/>,
link: `/lists/${data[0]}/fields`
});
}
if (perms.includes('viewSegments')) {
actions.push({
label: <Icon icon="tag" title={t('Segments')}/>,
link: `/lists/${data[0]}/segments`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,

View file

@ -40,18 +40,28 @@ export default class List extends Component {
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') },
{
actions: data => [{
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
}]
actions: data => {
const actions = [];
if (this.props.list.permissions.includes('manageFields')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
});
}
return actions;
}
}
];
return (
<div>
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
</Toolbar>
{this.props.list.permissions.includes('manageFields') &&
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
</Toolbar>
}
<Title>{t('Fields')}</Title>

View file

@ -70,7 +70,7 @@ function getMenus(t) {
fields: {
title: t('Fields'),
link: params => `/lists/${params.listId}/fields/`,
visible: resolved => resolved.list.permissions.includes('manageFields'),
visible: resolved => resolved.list.permissions.includes('viewFields'),
panelRender: props => <FieldsList list={props.resolved.list} />,
children: {
':fieldId([0-9]+)': {
@ -100,7 +100,7 @@ function getMenus(t) {
segments: {
title: t('Segments'),
link: params => `/lists/${params.listId}/segments`,
visible: resolved => resolved.list.permissions.includes('manageSegments'),
visible: resolved => resolved.list.permissions.includes('viewSegments'),
panelRender: props => <SegmentsList list={props.resolved.list} />,
children: {
':segmentId([0-9]+)': {

View file

@ -32,18 +32,28 @@ export default class List extends Component {
const columns = [
{ data: 1, title: t('Name') },
{
actions: data => [{
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/lists/${this.props.list.id}/segments/${data[0]}/edit`
}]
actions: data => {
const actions = [];
if (this.props.list.permissions.includes('manageSegments')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/lists/${this.props.list.id}/segments/${data[0]}/edit`
});
}
return actions;
}
}
];
return (
<div>
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/segments/create`} className="btn-primary" icon="plus" label={t('Create Segment')}/>
</Toolbar>
{this.props.list.permissions.includes('manageSegments') &&
<Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/segments/create`} className="btn-primary" icon="plus" label={t('Create Segment')}/>
</Toolbar>
}
<Title>{t('Segment')}</Title>

View file

@ -37,8 +37,8 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
const initVals = templateTypes[templateType].initData();
for (const key in initVals) {
if (!mutState.hasIn([prefix + key])) {
mutState.setIn([prefix + key, 'value'], initVals[key]);
if (!mutState.hasIn([key])) {
mutState.setIn([key, 'value'], initVals[key]);
}
}
}
@ -97,6 +97,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
};
},
beforeSave: data => {
console.log(data);
data[prefix + 'data'] = {
mosaicoTemplate: data[prefix + 'mosaicoTemplate'],
metadata: data[prefix + 'mosaicoData'].metadata,
@ -150,8 +151,8 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
});
},
initData: () => ({
mosaicoFsTemplate: mailtrainConfig.mosaico.fsTemplates[0][0],
mosaicoData: {}
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0][0],
[prefix + 'mosaicoData']: {}
}),
afterLoad: data => {
data[prefix + 'mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate;
@ -169,7 +170,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, 'mosaico');
initFieldsIfMissing(mutState, 'mosaicoWithFsTemplate');
},
validate: state => {}
};

View file

@ -34,7 +34,7 @@ function getMenus(t) {
title: t('Files'),
link: params => `/templates/${params.templateId}/files`,
visible: resolved => resolved.template.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Files')} entity={props.resolved.template} entityTypeId="template" entitySubTypeId="file" managePermission="manageFiles"/>
panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the content of the campaign.')} entity={props.resolved.template} entityTypeId="template" entitySubTypeId="file" managePermission="manageFiles"/>
},
share: {
title: t('Share'),
@ -70,13 +70,13 @@ function getMenus(t) {
title: t('Files'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Files')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="file" managePermission="manageFiles" />
panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the Mosaico template.')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="file" managePermission="manageFiles" />
},
blocks: {
title: t('Block thumbnails'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/blocks`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Block thumbnails')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="block" managePermission="manageFiles" />
panelRender: props => <Files title={t('Block thumbnails')} help={t('These files will be used by Mosaico to search for block thumbnails (the "edres" directory). Place here one file per block type that you have defined in the Mosaico template. Each file must have the same name as the block id. The file will be used as the thumbnail of the corresponding block.')}entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="block" managePermission="manageFiles" />
},
share: {
title: t('Share'),