- Fix for #890
- "Channels" feature - Shoutout config param rendered on the homepage - "Clone" feature for campaigns
This commit is contained in:
parent
82251d1cb9
commit
00432e6cfe
23 changed files with 1691 additions and 494 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
123
client/src/campaigns/Clone.js
Normal file
123
client/src/campaigns/Clone.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
105
client/src/channels/List.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
79
client/src/channels/root.js
Normal file
79
client/src/channels/root.js
Normal 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
|
||||
}
|
0
client/src/channels/styles.scss
Normal file
0
client/src/channels/styles.scss
Normal file
24
client/src/lib/bootstrap-components.js
vendored
24
client/src/lib/bootstrap-components.js
vendored
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit f034b066a787aa87c5fd378fd09e5bce8f2e5618
|
||||
Subproject commit 6daafcfcf20efd3c7aac6b1170bc22180d6cde24
|
|
@ -40,6 +40,7 @@ const usersRest = require('./routes/rest/users');
|
|||
const accountRest = require('./routes/rest/account');
|
||||
const reportTemplatesRest = require('./routes/rest/report-templates');
|
||||
const reportsRest = require('./routes/rest/reports');
|
||||
const channelsRest = require('./routes/rest/channels');
|
||||
const campaignsRest = require('./routes/rest/campaigns');
|
||||
const triggersRest = require('./routes/rest/triggers');
|
||||
const listsRest = require('./routes/rest/lists');
|
||||
|
@ -300,6 +301,7 @@ async function createApp(appType) {
|
|||
app.use('/rest', sendConfigurationsRest);
|
||||
app.use('/rest', usersRest);
|
||||
app.use('/rest', accountRest);
|
||||
app.use('/rest', channelsRest);
|
||||
app.use('/rest', campaignsRest);
|
||||
app.use('/rest', triggersRest);
|
||||
app.use('/rest', listsRest);
|
||||
|
|
|
@ -293,49 +293,53 @@ defaultRoles:
|
|||
master:
|
||||
name: Master
|
||||
description: All permissions
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createChannel, createCampaign, manageUsers]
|
||||
children:
|
||||
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
reportTemplate: [view, edit, delete, share, execute]
|
||||
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createChannel, createCampaign, manageUsers]
|
||||
|
||||
campaignsAdmin:
|
||||
name: Campaigns Admin
|
||||
description: In the respective namespace, the user has all permissions for managing lists, templates and campaigns and the permission to send to send configurations.
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createCampaign]
|
||||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createChannel, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic, sendWithoutOverrides, sendWithAllowedOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
channel: [view, edit, delete, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles, sendToTestUsers]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
reportTemplate: [view, share, execute]
|
||||
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createCampaign]
|
||||
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createChannel, createCampaign]
|
||||
|
||||
campaignsCreator:
|
||||
name: Campaigns Creator
|
||||
description: In the respective namespace, the user has all permissions to create and manage templates and campaigns. The user can also read public data about send configurations and use Mosaico templates in the namespace.
|
||||
permissions: [view, createTemplate, createCampaign]
|
||||
permissions: [view, createTemplate, createChannel, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic]
|
||||
channel: [view]
|
||||
campaign: [view, edit, delete, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, viewFiles, manageFiles, sendToTestUsers]
|
||||
mosaicoTemplate: [view, viewFiles]
|
||||
namespace: [view, createTemplate, createCampaign]
|
||||
namespace: [view, createTemplate, createChannel, createCampaign]
|
||||
|
||||
campaignsViewer:
|
||||
name: Campaigns Viewer
|
||||
description: In the respective namespace, the user has permissions to view campaigns and templates in order to be able to replicate them.
|
||||
permissions: [view, createTemplate, createCampaign]
|
||||
children:
|
||||
channel: [view]
|
||||
campaign: [view, viewFiles, viewAttachments, viewTriggers]
|
||||
template: [view, viewFiles]
|
||||
mosaicoTemplate: [view, viewFiles]
|
||||
|
@ -371,6 +375,16 @@ defaultRoles:
|
|||
description: All permissions
|
||||
permissions: [view, edit, delete, share]
|
||||
|
||||
channel:
|
||||
master:
|
||||
name: Master
|
||||
description: All permissions
|
||||
permissions: [view, edit, delete, createCampaign, share]
|
||||
viewer:
|
||||
name: Viewer
|
||||
description: The user can view the channel but cannot edit it or delete it.
|
||||
permissions: [view]
|
||||
|
||||
campaign:
|
||||
master:
|
||||
name: Master
|
||||
|
|
|
@ -54,6 +54,12 @@ const entityTypes = {
|
|||
},
|
||||
clientLink: id => `/campaigns/${id}`
|
||||
},
|
||||
channel: {
|
||||
entitiesTable: 'channels',
|
||||
sharesTable: 'shares_channel',
|
||||
permissionsTable: 'permissions_channel',
|
||||
clientLink: id => `/channels/${id}`
|
||||
},
|
||||
template: {
|
||||
entitiesTable: 'templates',
|
||||
sharesTable: 'shares_template',
|
||||
|
|
|
@ -28,7 +28,7 @@ const lists = require('./lists');
|
|||
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
|
||||
const activityLog = require('../lib/activity-log');
|
||||
|
||||
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
||||
const allowedKeysCommon = ['name', 'description', 'namespace', 'channel',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||
|
||||
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
||||
|
@ -67,7 +67,7 @@ function hash(entity, content) {
|
|||
return hasher.hash(filteredEntity);
|
||||
}
|
||||
|
||||
async function _listDTAjax(context, namespaceId, params) {
|
||||
async function _listDTAjax(context, namespaceId, channelId, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||
|
@ -75,22 +75,30 @@ async function _listDTAjax(context, namespaceId, params) {
|
|||
builder => {
|
||||
builder = builder.from('campaigns')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||
.leftJoin('channels', 'channels.id', 'campaigns.channel')
|
||||
.whereNull('campaigns.parent');
|
||||
if (namespaceId) {
|
||||
builder = builder.where('namespaces.id', namespaceId);
|
||||
}
|
||||
if (channelId) {
|
||||
builder = builder.where('channels.id', channelId);
|
||||
}
|
||||
return builder;
|
||||
},
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'channels.name', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function listDTAjax(context, params) {
|
||||
return await _listDTAjax(context, undefined, params);
|
||||
return await _listDTAjax(context, undefined, undefined, params);
|
||||
}
|
||||
|
||||
async function listByNamespaceDTAjax(context, namespaceId, params) {
|
||||
return await _listDTAjax(context, namespaceId, params);
|
||||
return await _listDTAjax(context, namespaceId, undefined, params);
|
||||
}
|
||||
|
||||
async function listByChannelDTAjax(context, channelId, params) {
|
||||
return await _listDTAjax(context, undefined, channelId, params);
|
||||
}
|
||||
|
||||
async function listChildrenDTAjax(context, campaignId, params) {
|
||||
|
@ -1102,6 +1110,7 @@ module.exports.Content = Content;
|
|||
module.exports.hash = hash;
|
||||
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listByChannelDTAjax = listByChannelDTAjax;
|
||||
module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
|
||||
module.exports.listChildrenDTAjax = listChildrenDTAjax;
|
||||
module.exports.listWithContentDTAjax = listWithContentDTAjax;
|
||||
|
|
206
server/models/channels.js
Normal file
206
server/models/channels.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const hasher = require('node-object-hash')();
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||
const shortid = require('shortid');
|
||||
const { enforce, filterObject } = require('../lib/helpers');
|
||||
const shares = require('./shares');
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
const { allTagLanguages } = require('../../shared/templates');
|
||||
const { CampaignSource, } = require('../../shared/campaigns');
|
||||
const segments = require('./segments');
|
||||
const dependencyHelpers = require('../lib/dependency-helpers');
|
||||
|
||||
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
|
||||
const activityLog = require('../lib/activity-log');
|
||||
|
||||
const allowedKeys = ['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source'];
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
let filteredEntity;
|
||||
|
||||
filteredEntity = filterObject(entity, allowedKeys);
|
||||
filteredEntity.lists = entity.lists;
|
||||
|
||||
return hasher.hash(filteredEntity);
|
||||
}
|
||||
|
||||
async function listDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'channel', requiredOperations: ['view'] }],
|
||||
params,
|
||||
builder => {
|
||||
builder = builder.from('channels')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'channels.namespace');
|
||||
return builder;
|
||||
},
|
||||
['channels.id', 'channels.name', 'channels.cid', 'channels.description', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function _getByTx(tx, key, id, withPermissions = true) {
|
||||
const entity = await tx('channels').where('channels.' + key, id)
|
||||
.leftJoin('channel_lists', 'channels.id', 'channel_lists.channel')
|
||||
.groupBy('channels.id')
|
||||
.select([
|
||||
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.source',
|
||||
'channels.send_configuration', 'channels.from_name_override', 'channels.from_email_override', 'channels.reply_to_override', 'channels.subject',
|
||||
'channels.data', 'channels.click_tracking_disabled', 'channels.open_tracking_disabled', 'channels.unsubscribe_url',
|
||||
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', channel_lists.list, channel_lists.segment) ORDER BY channel_lists.id SEPARATOR \';\') as lists`)
|
||||
])
|
||||
.first();
|
||||
|
||||
if (!entity) {
|
||||
throw new shares.throwPermissionDenied();
|
||||
}
|
||||
|
||||
if (entity.lists) {
|
||||
entity.lists = entity.lists.split(';').map(x => {
|
||||
const entries = x.split(':');
|
||||
const list = Number.parseInt(entries[0]);
|
||||
const segment = entries[1] ? Number.parseInt(entries[1]) : null;
|
||||
return {list, segment};
|
||||
});
|
||||
} else {
|
||||
entity.lists = [];
|
||||
}
|
||||
|
||||
entity.data = JSON.parse(entity.data);
|
||||
|
||||
if (withPermissions) {
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'channel', id);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function getByIdTx(tx, context, id, withPermissions = true) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'view');
|
||||
|
||||
return await _getByTx(tx, 'id', id, withPermissions);
|
||||
}
|
||||
|
||||
async function getById(context, id, withPermissions = true) {
|
||||
return await knex.transaction(async tx => {
|
||||
return await getByIdTx(tx, context, id, withPermissions);
|
||||
});
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(tx, context, entity, isCreate) {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
if (entity.source !== null) {
|
||||
enforce(Number.isInteger(entity.source));
|
||||
|
||||
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
|
||||
} else if (entity.source === CampaignSource.CUSTOM) {
|
||||
enforce(allTagLanguages.includes(entity.data.sourceCustom.tag_language), `Invalid tag language '${entity.data.sourceCustom.tag_language}'`);
|
||||
} else {
|
||||
enforce(false, 'Unknown channel source');
|
||||
}
|
||||
}
|
||||
|
||||
for (const lstSeg of entity.lists) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', lstSeg.list, 'view');
|
||||
|
||||
if (lstSeg.segment) {
|
||||
// Check that the segment under the list exists
|
||||
await segments.getByIdTx(tx, context, lstSeg.list, lstSeg.segment);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.send_configuration) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', entity.send_configuration, 'viewPublic');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function _createTx(tx, context, entity, content) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
|
||||
|
||||
await _validateAndPreprocess(tx, context, entity, true, content);
|
||||
|
||||
const filteredEntity = filterObject(entity, allowedKeys);
|
||||
filteredEntity.cid = shortid.generate();
|
||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
|
||||
const ids = await tx('channels').insert(filteredEntity);
|
||||
const id = ids[0];
|
||||
|
||||
await tx('channel_lists').insert(entity.lists.map(x => ({channel: id, ...x})));
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'channel', entityId: id });
|
||||
|
||||
await activityLog.logEntityActivity('channel', EntityActivityType.CREATE, id, {});
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function create(context, entity) {
|
||||
return await knex.transaction(async tx => {
|
||||
return await _createTx(tx, context, entity);
|
||||
});
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.id, 'edit');
|
||||
|
||||
const existing = await _getByTx(tx, 'id', entity.id, false);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
await _validateAndPreprocess(tx, context, entity, false);
|
||||
|
||||
let filteredEntity = filterObject(entity, allowedKeys);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'channel', 'createCampaign', 'delete');
|
||||
|
||||
await tx('channel_lists').where('channel', entity.id).del();
|
||||
await tx('channel_lists').insert(entity.lists.map(x => ({channel: entity.id, ...x})));
|
||||
|
||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
await tx('channels').where('id', entity.id).update(filteredEntity);
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'channel', entityId: entity.id });
|
||||
|
||||
await activityLog.logEntityActivity('channel', EntityActivityType.UPDATE, entity.id, {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function remove(context, id) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'delete');
|
||||
|
||||
await dependencyHelpers.ensureNoDependencies(tx, context, id, [
|
||||
{ entityTypeId: 'campaign', column: 'channel' }
|
||||
]);
|
||||
|
||||
await tx('channels').where('id', id).del();
|
||||
|
||||
await activityLog.logEntityActivity('channel', EntityActivityType.REMOVE, id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports.hash = hash;
|
||||
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.getByIdTx = getByIdTx;
|
||||
module.exports.getById = getById;
|
||||
module.exports.create = create;
|
||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||
module.exports.remove = remove;
|
|
@ -735,6 +735,8 @@ async function _removeAndGetTx(tx, context, listId, existing) {
|
|||
if (existing.status === SubscriptionStatus.SUBSCRIBED) {
|
||||
await tx('lists').where('id', listId).decrement('subscribers', 1);
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
async function remove(context, listId, id) {
|
||||
|
|
|
@ -11,6 +11,10 @@ router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => {
|
|||
return res.json(await campaigns.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/campaigns-by-channel-table/:channelId', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.listByChannelDTAjax(req.context, castToInteger(req.params.channelId), req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
|
||||
});
|
||||
|
|
38
server/routes/rest/channels.js
Normal file
38
server/routes/rest/channels.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../../lib/passport');
|
||||
const channels = require('../../models/channels');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
const {castToInteger} = require('../../lib/helpers');
|
||||
|
||||
|
||||
router.postAsync('/channels-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await channels.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.getAsync('/channels/:channelId', passport.loggedIn, async (req, res) => {
|
||||
const channel = await channels.getById(req.context, castToInteger(req.params.channelId), true);
|
||||
channel.hash = channels.hash(channel);
|
||||
return res.json(channel);
|
||||
});
|
||||
|
||||
router.postAsync('/channels', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
return res.json(await channels.create(req.context, req.body));
|
||||
});
|
||||
|
||||
router.putAsync('/channels/:channelId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const entity = req.body;
|
||||
entity.id = castToInteger(req.params.channelId);
|
||||
|
||||
await channels.updateWithConsistencyCheck(req.context);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/channels/:channelId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await channels.remove(req.context, castToInteger(req.params.channelId));
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
54
server/setup/knex/migrations/20200617172500_add_channels.js
Normal file
54
server/setup/knex/migrations/20200617172500_add_channels.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.createTable('channels', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('cid').unique().collate('utf8_general_ci');
|
||||
table.string('name');
|
||||
table.text('description');
|
||||
table.string('cpg_name');
|
||||
table.text('cpg_description');
|
||||
table.string('from_email_override');
|
||||
table.string('from_name_override');
|
||||
table.string('reply_to_override');
|
||||
table.string('subject');
|
||||
table.integer('send_configuration').unsigned().references(`send_configurations.id`);
|
||||
table.integer('source').unsigned().notNullable();
|
||||
table.text('data', 'longtext');
|
||||
table.boolean('click_tracking_disabled').defaultTo(false);
|
||||
table.boolean('open_tracking_disabled').defaultTo(false);
|
||||
table.string('unsubscribe_url');
|
||||
table.timestamp('created').defaultTo(knex.fn.now());
|
||||
table.integer('namespace').unsigned().references('namespaces.id');
|
||||
|
||||
});
|
||||
|
||||
await knex.schema.createTable('channel_lists', table => {
|
||||
table.increments('id').primary();
|
||||
table.integer('channel').unsigned().notNullable().references('channels.id');
|
||||
table.integer('list').unsigned().notNullable().references('lists.id');
|
||||
table.integer('segment').unsigned().references('segments.id');
|
||||
});
|
||||
|
||||
await knex.schema.table('campaigns', table => {
|
||||
table.integer('channel').unsigned().references('channels.id');
|
||||
});
|
||||
|
||||
const entityType = 'channel';
|
||||
await knex.schema
|
||||
.createTable(`shares_${entityType}`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('role', 128).notNullable();
|
||||
table.boolean('auto').defaultTo(false);
|
||||
table.primary(['entity', 'user']);
|
||||
})
|
||||
.createTable(`permissions_${entityType}`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('operation', 128).notNullable();
|
||||
table.primary(['entity', 'user', 'operation']);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
768
zone-mta/package-lock.json
generated
768
zone-mta/package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue