- "Channels" feature
- Shoutout config param rendered on the homepage
- "Clone" feature for campaigns
This commit is contained in:
Tomas Bures 2020-06-20 08:09:23 +02:00
parent 82251d1cb9
commit 00432e6cfe
23 changed files with 1691 additions and 494 deletions

View file

@ -1,6 +1,7 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {withTranslation} from './lib/i18n';
import {requiresAuthenticatedUser} from './lib/page';
import {withComponentMixins} from "./lib/decorator-helpers";
@ -14,6 +15,10 @@ export default class List extends Component {
super(props);
}
static propTypes = {
configItems: PropTypes.object
}
render() {
const t = this.props.t;
@ -21,6 +26,7 @@ export default class List extends Component {
<div>
<h2>{t('Mailtrain 2 beta')}</h2>
<div>{t('Build') + ' 2020-05-28-0102'}</div>
<p>{this.props.configItems.shoutout}</p>
</div>
);
}

View file

@ -79,9 +79,20 @@ export default class CUD extends Component {
[CampaignSource.URL]: t('url')
};
const sourceLabelsOrder = [
CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_CAMPAIGN , CampaignSource.TEMPLATE, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.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
];
}
this.sourceOptions = [];
for (const key of sourceLabelsOrder) {
@ -116,6 +127,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
entity: PropTypes.object,
createFromChannel: PropTypes.object,
createFromCampaign: PropTypes.object,
permissions: PropTypes.object,
type: PropTypes.number
}
@ -298,61 +311,209 @@ export default class CUD extends Component {
}
} 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,
if (this.props.createFromChannel) {
const channel = this.props.createFromChannel;
type: this.props.type,
for (const overridable of campaignOverridables) {
if (channel[overridable + '_override'] === null) {
data[overridable + '_override'] = '';
data[overridable + '_overriden'] = false;
} else {
data[overridable + '_override'] = channel[overridable + '_override'];
data[overridable + '_overriden'] = true;
}
}
name: '',
description: '',
const lsts = [];
for (const lst of channel.lists) {
const lstUid = this.getNextListEntryId();
[lstPrefix + 'list']: null,
[lstPrefix + 'segment']: null,
[lstPrefix + 'useSegmentation']: false,
lists: [lstUid],
const prefix = 'lists_' + lstUid + '_';
send_configuration: null,
namespace: getDefaultNamespace(this.props.permissions),
data[prefix + 'list'] = lst.list;
data[prefix + 'segment'] = lst.segment;
data[prefix + 'useSegmentation'] = !!lst.segment;
subject: '',
lsts.push(lstUid);
}
data.lists = lsts;
click_tracking_disabled: false,
open_tracking_disabled: false,
data.type = CampaignType.REGULAR;
unsubscribe_url: '',
data.name = channel.cpg_name;
data.description = channel.cpg_description;
data.send_configuration = channel.send_configuration;
if (channel.send_configuration) {
// noinspection JSIgnoredPromiseFromCall
this.fetchSendConfiguration(channel.send_configuration);
}
data.namespace = channel.namespace;
data.subject = channel.subject;
data.click_tracking_disabled = channel.click_tracking_disabled;
data.open_tracking_disabled = channel.open_tracking_disabled;
data.unsubscribe_url = channel.unsubscribe_url;
data.source = channel.source;
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;
}
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 (this.props.createFromCampaign) {
const sourceCampaign = this.props.createFromCampaign;
for (const overridable of campaignOverridables) {
if (sourceCampaign[overridable + '_override'] === null) {
data[overridable + '_override'] = '';
data[overridable + '_overriden'] = false;
} else {
data[overridable + '_override'] = sourceCampaign[overridable + '_override'];
data[overridable + '_overriden'] = true;
}
}
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;
data.type = sourceCampaign.type;
data.name = sourceCampaign.name;
data.description = sourceCampaign.description;
data.send_configuration = sourceCampaign.send_configuration;
// noinspection JSIgnoredPromiseFromCall
this.fetchSendConfiguration(sourceCampaign.send_configuration);
data.namespace = sourceCampaign.namespace;
data.subject = sourceCampaign.subject;
data.click_tracking_disabled = sourceCampaign.click_tracking_disabled;
data.open_tracking_disabled = sourceCampaign.open_tracking_disabled;
data.unsubscribe_url = sourceCampaign.unsubscribe_url;
source: CampaignSource.CUSTOM,
// This is for CampaignSource.TEMPLATE and CampaignSource.CUSTOM_FROM_TEMPLATE
data_sourceTemplate: null,
data.data_sourceTemplate = null;
// This is for CampaignSource.CUSTOM_FROM_CAMPAIGN
data_sourceCampaign: null,
data.data_sourceCampaign = null;
// This is for CampaignSource.CUSTOM
data_sourceCustom_type: mailtrainConfig.editors[0],
data_sourceCustom_tag_language: mailtrainConfig.tagLanguages[0],
data_sourceCustom_data: {},
data_sourceCustom_html: '',
data_sourceCustom_text: '',
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 = '';
...this.templateTypes[mailtrainConfig.editors[0]].initData(),
Object.assign(data, this.templateTypes[mailtrainConfig.editors[0]].initData());
// This is for CampaignSource.URL
data_sourceUrl: '',
data.data_sourceUrl = '';
// This is for CampaignType.RSS
data_feedUrl: ''
});
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;
} else if (sourceCampaign.source === CampaignSource.TEMPLATE) {
data.source = CampaignSource.TEMPLATE;
data.data_sourceTemplate = sourceCampaign.data.sourceTemplate;
} else if (sourceCampaign.source === CampaignSource.URL) {
data.source = CampaignSource.URL;
data.data_sourceUrl = sourceCampaign.data.sourceUrl;
}
} else {
for (const overridable of campaignOverridables) {
data[overridable + '_override'] = '';
data[overridable + '_overriden'] = false;
}
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];
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 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);
}
}

View file

@ -0,0 +1,123 @@
'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, CampaignType} from "../../../shared/campaigns";
import moment from 'moment';
import {getMailerTypes} from "../send-configurations/helpers";
import {getCampaignLabels} from "./helpers";
import {withComponentMixins} from "../lib/decorator-helpers";
import interoperableErrors from "../../../shared/interoperable-errors";
import {Trans} from "react-i18next";
import {Table} from "../lib/table";
@withComponentMixins([
withTranslation,
withForm,
withErrorHandling,
withPageHelpers,
requiresAuthenticatedUser
])
export default class Clone extends Component {
constructor(props) {
super(props);
const t = props.t;
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN);
this.tagLanguages = getTagLanguages(props.t);
this.mailerTypes = getMailerTypes(props.t);
const { campaignTypeLabels } = getCampaignLabels(t);
this.campaignTypeLabels = campaignTypeLabels;
this.initForm({
leaveConfirmation: false,
});
}
static propTypes = {
}
componentDidMount() {
this.populateFormValues({
sourceCampaign: null
});
}
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(['sourceCampaign', 'value'])) {
state.setIn(['sourceCampaign', 'error'], t('campaignMustBeSelected'));
}
}
@withFormErrorHandlers
async submitHandler(afterSubmitAction) {
const t = this.props.t;
const sourceCampaign = this.getFormValue('sourceCampaign');
this.navigateTo(`/campaigns/clone/${sourceCampaign}`);
}
render() {
const t = this.props.t;
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: 9, title: t('created'), render: data => moment(data).fromNow() },
{ data: 10, title: t('namespace') }
];
return (
<div>
<Title>{t('Create campaign')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<TableSelect id="sourceCampaign" label={t('campaign')} withHeader dropdown dataUrl='rest/campaigns-table' columns={campaignsColumns} order={[4, 'desc']} selectionLabelIndex={1} help={t('Select campaign to be cloned.')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="chevron-right" label={t('Next')}/>
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -3,7 +3,7 @@
import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n';
import {ButtonDropdown, Icon} from '../lib/bootstrap-components';
import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {DropdownLink, LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table';
import moment from 'moment';
@ -35,126 +35,156 @@ export default class List extends Component {
}
static propTypes = {
permissions: PropTypes.object
permissions: PropTypes.object,
channel: PropTypes.object
}
render() {
const t = this.props.t;
const channel = this.props.channel;
const permissions = this.props.permissions;
const createPermitted = permissions.createCampaign;
const columns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('id'), render: data => <code>{data}</code>, className: styles.tblCol_id },
{ data: 3, title: t('description') },
{ data: 4, title: t('type'), render: data => this.campaignTypeLabels[data] },
{
data: 5,
title: t('status'),
render: (data, display, rowData) => {
if (data === CampaignStatus.SCHEDULED) {
const scheduled = rowData[6];
if (scheduled && new Date(scheduled) > new Date()) {
return t('sendingScheduled');
} else {
return t('sending');
}
} else {
return this.campaignStatusLabels[data];
}
}
},
{ data: 8, title: t('created'), render: data => moment(data).fromNow() },
{ data: 9, title: t('namespace') },
{
className: styles.tblCol_buttons,
actions: data => {
const actions = [];
const perms = data[10];
const campaignType = data[4];
const status = data[5];
const campaignSource = data[7];
if (perms.includes('view')) {
actions.push({
label: <Icon icon="envelope" title={t('status')}/>,
link: `/campaigns/${data[0]}/status`
});
}
if (perms.includes('viewStats')) {
actions.push({
label: <Icon icon="signal" title={t('statistics')}/>,
link: `/campaigns/${data[0]}/statistics`
});
}
if (perms.includes('view') || perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('edit')}/>,
link: `/campaigns/${data[0]}/edit`
});
}
if (perms.includes('edit') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
actions.push({
label: <Icon icon="align-center" title={t('content')}/>,
link: `/campaigns/${data[0]}/content`
});
}
if (perms.includes('viewFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
actions.push({
label: <Icon icon="hdd" title={t('files')}/>,
link: `/campaigns/${data[0]}/files`
});
}
if (perms.includes('viewAttachments')) {
actions.push({
label: <Icon icon="paperclip" title={t('attachments')}/>,
link: `/campaigns/${data[0]}/attachments`
});
}
if (campaignType === CampaignType.TRIGGERED && perms.includes('viewTriggers')) {
actions.push({
label: <Icon icon="bell" title={t('triggers')}/>,
link: `/campaigns/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share" title={t('share')}/>,
link: `/campaigns/${data[0]}/share`
});
}
tableAddDeleteButton(actions, this, perms, `rest/campaigns/${data[0]}`, data[1], t('deletingCampaign'), t('campaignDeleted'));
return actions;
const columns = [];
columns.push({
data: 1,
title: t('name'),
actions: data => {
const perms = data[10];
if (perms.includes('view')) {
return [{label: data[1], link: `/campaigns/${data[0]}/status`}];
} else {
return [{label: data[1]}];
}
}
];
});
columns.push({ data: 2, title: t('id'), render: data => <code>{data}</code>, className: styles.tblCol_id });
columns.push({ data: 3, title: t('description') });
columns.push({ data: 4, title: t('type'), render: data => this.campaignTypeLabels[data] });
if (!channel) {
columns.push({ data: 5, title: t('Channel') });
}
columns.push({
data: 6,
title: t('status'),
render: (data, display, rowData) => {
if (data === CampaignStatus.SCHEDULED) {
const scheduled = rowData[6];
if (scheduled && new Date(scheduled) > new Date()) {
return t('sendingScheduled');
} else {
return t('sending');
}
} else {
return this.campaignStatusLabels[data];
}
}
});
columns.push({ data: 9, title: t('created'), render: data => moment(data).fromNow() });
columns.push({ data: 10, title: t('namespace') });
columns.push({
className: styles.tblCol_buttons,
actions: data => {
const actions = [];
const perms = data[11];
const campaignType = data[4];
const campaignSource = data[8];
if (perms.includes('view')) {
actions.push({
label: <Icon icon="envelope" title={t('status')}/>,
link: `/campaigns/${data[0]}/status`
});
}
if (perms.includes('viewStats')) {
actions.push({
label: <Icon icon="signal" title={t('statistics')}/>,
link: `/campaigns/${data[0]}/statistics`
});
}
if (perms.includes('view') || perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('edit')}/>,
link: `/campaigns/${data[0]}/edit`
});
}
if (perms.includes('edit') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
actions.push({
label: <Icon icon="align-center" title={t('content')}/>,
link: `/campaigns/${data[0]}/content`
});
}
if (perms.includes('viewFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
actions.push({
label: <Icon icon="hdd" title={t('files')}/>,
link: `/campaigns/${data[0]}/files`
});
}
if (perms.includes('viewAttachments')) {
actions.push({
label: <Icon icon="paperclip" title={t('attachments')}/>,
link: `/campaigns/${data[0]}/attachments`
});
}
if (campaignType === CampaignType.TRIGGERED && perms.includes('viewTriggers')) {
actions.push({
label: <Icon icon="bell" title={t('triggers')}/>,
link: `/campaigns/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share" title={t('share')}/>,
link: `/campaigns/${data[0]}/share`
});
}
tableAddDeleteButton(actions, this, perms, `rest/campaigns/${data[0]}`, data[1], t('deletingCampaign'), t('campaignDeleted'));
return actions;
}
});
let createButton = null;
if (createPermitted) {
if (channel) {
createButton = <LinkButton to={`/channels/${channel.id}/create`} className="btn-primary" icon="plus" label={t('createCampaign')}/>;
} else {
createButton = (
<>
<LinkButton to={`/campaigns/clone`} className="btn-primary" icon="clone" label={t('Clone Campaign')}/>
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" icon="plus" label={t('createCampaign')}>
<DropdownLink to="/campaigns/create-regular">{t('regular')}</DropdownLink>
<DropdownLink to="/campaigns/create-rss">{t('rss')}</DropdownLink>
<DropdownLink to="/campaigns/create-triggered">{t('triggered')}</DropdownLink>
</ButtonDropdown>
</>
);
}
}
return (
<div>
{tableRestActionDialogRender(this)}
<Toolbar>
{createPermitted &&
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createCampaign')}>
<DropdownLink to="/campaigns/create-regular">{t('regular')}</DropdownLink>
<DropdownLink to="/campaigns/create-rss">{t('rss')}</DropdownLink>
<DropdownLink to="/campaigns/create-triggered">{t('triggered')}</DropdownLink>
</ButtonDropdown>
}
{createButton}
</Toolbar>
<Title>{t('campaigns')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} order={[5, 'desc']} />
<Table ref={node => this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} order={[6, 'desc']} />
</div>
);
}

View file

@ -18,6 +18,7 @@ import StatisticsOpened from "./StatisticsOpened";
import StatisticsLinkClicks from "./StatisticsLinkClicks";
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"
import {namespaceCheckPermissions} from "../lib/namespace";
import Clone from "./Clone";
function getMenus(t) {
const aggLabels = {
@ -25,6 +26,12 @@ function getMenus(t) {
'devices': t('devices')
};
const createLabels = {
[CampaignType.REGULAR]: t('createRegularCampaign'),
[CampaignType.RSS]: t('createRssCampaign'),
[CampaignType.TRIGGERED]: t('createTriggeredCampaign')
};
return {
'campaigns': {
title: t('campaigns'),
@ -160,16 +167,30 @@ function getMenus(t) {
}
},
'create-regular': {
title: t('createRegularCampaign'),
title: createLabels[CampaignType.REGULAR],
panelRender: props => <CampaignsCUD action="create" type={CampaignType.REGULAR} permissions={props.permissions} />
},
'create-rss': {
title: t('createRssCampaign'),
title: createLabels[CampaignType.RSS],
panelRender: props => <CampaignsCUD action="create" type={CampaignType.RSS} permissions={props.permissions} />
},
'create-triggered': {
title: t('createTriggeredCampaign'),
title: createLabels[CampaignType.TRIGGERED],
panelRender: props => <CampaignsCUD action="create" type={CampaignType.TRIGGERED} permissions={props.permissions} />
},
'clone': {
title: t('Create Campaign'),
link: params => `/campaigns/clone`,
panelRender: props => <Clone />,
children: {
':existingCampaignId([0-9]+)': {
title: resolved => createLabels[resolved.existingCampaign.type],
resolve: {
existingCampaign: params => `rest/campaigns-settings/${params.existingCampaignId}`
},
panelRender: props => <CampaignsCUD action="create" createFromCampaign={props.resolved.existingCampaign} permissions={props.permissions} />
}
}
}
}
}

105
client/src/channels/List.js Normal file
View file

@ -0,0 +1,105 @@
'use strict';
import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n';
import {Icon} from '../lib/bootstrap-components';
import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table';
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers";
import styles from "./styles.scss";
import PropTypes from 'prop-types';
@withComponentMixins([
withTranslation,
withErrorHandling,
withPageHelpers,
requiresAuthenticatedUser
])
export default class List extends Component {
constructor(props) {
super(props);
const t = props.t;
this.state = {};
tableRestActionDialogInit(this);
}
static propTypes = {
permissions: PropTypes.object
}
render() {
const t = this.props.t;
const permissions = this.props.permissions;
const createPermitted = permissions.createChannel;
const columns = [
{
data: 1,
title: t('name'),
actions: data => {
const perms = data[10];
if (perms.includes('view')) {
return [{label: data[1], link: `/channels/${data[0]}/campaigns`}];
} else {
return [{label: data[1]}];
}
}
},
{ data: 2, title: t('id'), render: data => <code>{data}</code> },
{ data: 3, title: t('description') },
{ data: 4, title: t('namespace') },
{
className: styles.tblCol_buttons,
actions: data => {
const actions = [];
const perms = data[5];
if (perms.includes('view')) {
actions.push({
label: <Icon icon="inbox" title={t('Campaigns')}/>,
link: `/channels/${data[0]}/campaigns`
});
}
if (perms.includes('view') || perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('edit')}/>,
link: `/channels/${data[0]}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share" title={t('share')}/>,
link: `/channels/${data[0]}/share`
});
}
tableAddDeleteButton(actions, this, perms, `rest/channels/${data[0]}`, data[1], t('Deleting channel ...'), t('Channel deleted'));
return actions;
}
}
];
return (
<div>
{tableRestActionDialogRender(this)}
<Toolbar>
{createPermitted &&
<LinkButton to="/channels/create" className="btn-primary" icon="plus" label={t('Create Channel')}/>
}
</Toolbar>
<Title>{t('Channels')}</Title>
<Table ref={node => this.table = node} withHeader dataUrl="rest/channels-table" columns={columns} />
</div>
);
}
}

View file

@ -0,0 +1,79 @@
'use strict';
import React from 'react';
import CampaignsList from '../campaigns/List';
import CampaignsCUD from '../campaigns/CUD';
import ChannelsList from './List';
//import ChannelsCUD from './CUD';
import Share from '../shares/Share';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers"
import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) {
return {
'channels': {
title: t('Channels'),
link: '/channels',
checkPermissions: {
createChannel: {
entityTypeId: 'namespace',
requiredOperations: ['createChannel']
},
createCampaign: {
entityTypeId: 'namespace',
requiredOperations: ['createCampaign']
},
...namespaceCheckPermissions('createChannel'),
},
panelRender: props => <ChannelsList permissions={props.permissions}/>,
children: {
':channelId([0-9]+)': {
title: resolved => t('channelName', {name: ellipsizeBreadcrumbLabel(resolved.channel.name)}),
resolve: {
channel: params => `rest/channels/${params.channelId}`
},
link: params => `/channels/${params.channelId}/campaigns`,
navs: {
campaigns: {
title: t('Campaigns'),
link: params => `/channels/${params.channelId}/campaigns`,
visible: resolved => resolved.channel.permissions.includes('view'),
panelRender: props => <CampaignsList channel={props.resolved.channel} />
},
/*
':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`,
visible: resolved => resolved.channel.permissions.includes('share'),
panelRender: props => <Share title={t('share')} entity={props.resolved.channel} entityTypeId="channel" />
}
},
children: {
create: {
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} />
}
}
},
'create': {
title: t('Create Channel'),
panelRender: props => <ChannelsCUD action="create" permissions={props.permissions} />
}
}
}
};
}
export default {
getMenus
}

View file

View file

@ -111,6 +111,7 @@ export class Button extends Component {
export class ButtonDropdown extends Component {
static propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
icon: PropTypes.string,
className: PropTypes.string,
buttonClassName: PropTypes.string,
menuClassName: PropTypes.string
@ -119,19 +120,24 @@ export class ButtonDropdown extends Component {
render() {
const props = this.props;
const className = 'dropdown' + (props.className ? ' ' + props.className : '');
const className = 'btn-group' + (props.className ? ' ' + props.className : '');
const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : '');
const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
return (
<div className="dropdown" className={className}>
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{props.label}
</button>
<ul className={menuClassName}>
{props.children}
</ul>
let icon;
if (props.icon) {
icon = <Icon icon={props.icon}/>
}
let iconSpacer;
if (props.icon && props.label) {
iconSpacer = ' ';
}
return (
<div className={className}>
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{icon}{iconSpacer}{props.label}</button>
<ul className={menuClassName}>{props.children}</ul>
</div>
);
}

View file

@ -34,6 +34,8 @@ 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 = {
@ -929,6 +931,207 @@ 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
])
@ -984,6 +1187,7 @@ class TableSelect extends Component {
dataUrl: PropTypes.string,
data: PropTypes.array,
columns: PropTypes.array,
order: PropTypes.array,
selectionKeyIndex: PropTypes.number,
selectionLabelIndex: PropTypes.number,
selectionAsArray: PropTypes.bool,
@ -1076,6 +1280,7 @@ class TableSelect extends Component {
data={props.data}
dataUrl={props.dataUrl}
columns={props.columns}
order={props.order}
selectMode={props.selectMode}
selectionAsArray={this.props.selectionAsArray}
withHeader={props.withHeader}
@ -1094,6 +1299,7 @@ class TableSelect extends Component {
data={props.data}
dataUrl={props.dataUrl}
columns={props.columns}
order={props.order}
pageLength={props.pageLength}
selectMode={props.selectMode}
selectionAsArray={this.props.selectionAsArray}

View file

@ -163,6 +163,13 @@ export function getRoutes(structure, parentRoute) {
entryResolve = resolve;
}
let entryResolveWithLocal;
if (entry.localResolve) {
entryResolveWithLocal = Object.assign({}, entryResolve, entry.localResolve);
} else {
entryResolveWithLocal = entryResolve;
}
let entryCheckPermissions;
if (entry.checkPermissions) {
entryCheckPermissions = Object.assign({}, checkPermissions, entry.checkPermissions);
@ -199,7 +206,7 @@ export function getRoutes(structure, parentRoute) {
link: entry.link,
panelInFullScreen: entry.panelInFullScreen,
insideIframe: entry.insideIframe,
resolve: entryResolve,
resolve: entryResolveWithLocal,
checkPermissions: entryCheckPermissions,
parents,
navs: [...navs, ...entryNavs],

View file

@ -12,6 +12,7 @@ import lists from './lists/root';
import namespaces from './namespaces/root';
import reports from './reports/root';
import campaigns from './campaigns/root';
import channels from './channels/root';
import templates from './templates/root';
import users from './users/root';
import sendConfigurations from './send-configurations/root';
@ -25,8 +26,9 @@ import {DropdownActionLink, Icon} from "./lib/bootstrap-components";
import axios from './lib/axios';
import {getUrl} from "./lib/urls";
import {withComponentMixins} from "./lib/decorator-helpers";
import Update from "./settings/Update";
const topLevelMenuKeys = ['lists', 'templates', 'campaigns'];
const topLevelMenuKeys = ['lists', 'channels', 'templates', 'campaigns'];
if (mailtrainConfig.reportsEnabled) {
topLevelMenuKeys.push('reports');
@ -114,7 +116,10 @@ class Root extends Component {
structure = {
title: t('home'),
link: '/',
panelComponent: Home,
localResolve: {
configItems: params => `rest/settings`
},
panelRender: props => <Home configItems={props.resolved.configItems} />,
primaryMenuComponent: MainMenu,
children: {
...login.getMenus(t),
@ -127,7 +132,8 @@ class Root extends Component {
...account.getMenus(t),
...settings.getMenus(t),
...sendConfigurations.getMenus(t),
...campaigns.getMenus(t)
...campaigns.getMenus(t),
...channels.getMenus(t)
}
};