Editing of campaigns seems to work
This commit is contained in:
parent
b1c667d13d
commit
7b46c4b4b0
27 changed files with 335 additions and 130 deletions
|
@ -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');
|
||||
|
|
|
@ -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 ...'));
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}`)
|
||||
},
|
||||
|
|
|
@ -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 > * {
|
||||
|
|
|
@ -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')}/>,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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]+)': {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 => {}
|
||||
};
|
||||
|
|
|
@ -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'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue