Added CSV export of subscribers

Fixed some bugs in subscriptions
Updated some packages to avoid warnings about vulnerabilities
Completed RSS feed campaigns
This commit is contained in:
Tomas Bures 2018-11-17 02:54:23 +01:00
parent 8683f8c91e
commit bf69e633c4
47 changed files with 5255 additions and 9651 deletions

4984
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -61,7 +61,7 @@
"react-router-dom": "^4.1.1",
"react-sortable-tree": "^1.2.0",
"slugify": "^1.1.0",
"url-parse": "^1.1.9"
"url-parse": "^1.4.4"
},
"devDependencies": {
"@ckeditor/ckeditor5-dev-utils": "^11.0.1",
@ -76,12 +76,12 @@
"css-loader": "^0.28.4",
"file-loader": "^2.0.0",
"i18next-conv": "^3.0.3",
"node-sass": "^4.5.3",
"node-sass": "^4.10.0",
"postcss-loader": "^3.0.0",
"raw-loader": "^0.5.1",
"sass-loader": "^6.0.6",
"style-loader": "^0.18.2",
"url-loader": "^0.5.9",
"url-loader": "^1.1.2",
"webpack": "^2.6.1"
}
}

View file

@ -24,13 +24,21 @@ import {
} from '../lib/error-handling';
import {getCampaignLabels} from './helpers';
import {Table} from "../lib/table";
import {Button} from "../lib/bootstrap-components";
import {
Button,
Icon
} from "../lib/bootstrap-components";
import axios from "../lib/axios";
import {getUrl, getPublicUrl} from "../lib/urls";
import interoperableErrors from '../../../shared/interoperable-errors';
import {CampaignStatus} from "../../../shared/campaigns";
import {
CampaignSource,
CampaignStatus,
CampaignType
} from "../../../shared/campaigns";
import moment from 'moment';
import campaignsStyles from "./styles.scss";
import {tableDeleteDialogAddDeleteButton} from "../lib/modals";
@translate()
@ -207,10 +215,21 @@ class SendControls extends Component {
await this.refreshEntity();
}
async enableAsync() {
await this.postAndMaskStateError(`rest/campaign-enable/${this.props.entity.id}`);
await this.refreshEntity();
}
async disableAsync() {
await this.postAndMaskStateError(`rest/campaign-disable/${this.props.entity.id}`);
await this.refreshEntity();
}
render() {
const t = this.props.t;
const entity = this.props.entity;
console.log(entity);
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers')})`;
@ -266,6 +285,30 @@ class SendControls extends Component {
</ButtonRow>
</div>
);
} else if (entity.status === CampaignStatus.INACTIVE) {
return (
<div>
<AlignedRow label={t('Send status')}>
{t('Your campaign is currently disabled. Click Enable button to start enable it.')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="play" label={t('Enable')} onClickAsync={::this.enableAsync}/>
</ButtonRow>
</div>
);
} else if (entity.status === CampaignStatus.ACTIVE) {
return (
<div>
<AlignedRow label={t('Send status')}>
{t('Your campaign is enabled and sending messages.')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="stop" label={t('Disable')} onClickAsync={::this.disableAsync}/>
</ButtonRow>
</div>
);
} else {
return null;
@ -362,6 +405,30 @@ export default class Status extends Component {
{ data: 3, title: t('List namespace') }
];
const campaignsChildrenColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 5, title: t('Status'), render: (data, display, rowData) => this.campaignStatusLabels[data] },
{ data: 8, title: t('Created'), render: data => moment(data).fromNow() },
{
actions: data => {
const actions = [];
const perms = data[10];
const campaignType = data[4];
const campaignSource = data[7];
if (perms.includes('viewStats')) {
actions.push({
label: <Icon icon="send" title={t('Status')}/>,
link: `/campaigns/${data[0]}/status`
});
}
return actions;
}
}
];
return (
<div>
<Title>{t('Campaign Status')}</Title>
@ -383,6 +450,15 @@ export default class Status extends Component {
<hr/>
<SendControls entity={entity} refreshEntity={::this.refreshEntity}/>
{entity.type === CampaignType.RSS &&
<div>
<hr/>
<h3>RSS Entries</h3>
<p>{t('If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here')}</p>
<Table withHeader dataUrl={`rest/campaigns-children/${this.props.entity.id}`} columns={campaignsChildrenColumns} />
</div>
}
</div>
);
}

View file

@ -31,55 +31,4 @@ export function getCampaignLabels(t) {
};
}
/* FIXME - this is not used at the moment, but it's kept here because it will be probably needed at some later point of time.
export function getDefaultMergeTags(t) {
return [{
key: 'LINK_UNSUBSCRIBE',
value: t('URL that points to the unsubscribe page')
}, {
key: 'LINK_PREFERENCES',
value: t('URL that points to the preferences page of the subscriber')
}, {
key: 'LINK_BROWSER',
value: t('URL to preview the message in a browser')
}, {
key: 'EMAIL',
value: t('Email address')
}, {
key: 'SUBSCRIPTION_ID',
value: t('Unique ID that identifies the recipient')
}, {
key: 'LIST_ID',
value: t('Unique ID that identifies the list used for this campaign')
}, {
key: 'CAMPAIGN_ID',
value: t('Unique ID that identifies current campaign')
}];
}
export function getRSSMergeTags(t) {
return [{
key: 'RSS_ENTRY',
value: t('content from an RSS entry')
}, {
key: 'RSS_ENTRY_TITLE',
value: t('RSS entry title')
}, {
key: 'RSS_ENTRY_DATE',
value: t('RSS entry date')
}, {
key: 'RSS_ENTRY_LINK',
value: t('RSS entry link')
}, {
key: 'RSS_ENTRY_CONTENT',
value: t('content from an RSS entry')
}, {
key: 'RSS_ENTRY_SUMMARY',
value: t('RSS entry summary')
}, {
key: 'RSS_ENTRY_IMAGE_URL',
value: t('RSS entry image URL')
}];
}
*/

View file

@ -14,7 +14,7 @@ import {Icon, ModalDialog} from "./bootstrap-components";
import axios from './axios';
import styles from "./styles.scss";
import {withPageHelpers} from "./page";
import {getUrl} from "./urls";
import {getUrl, getPublicUrl} from "./urls";
@translate()
@withErrorHandling
@ -119,9 +119,9 @@ export default class Files extends Component {
let downloadUrl;
if (this.props.usePublicDownloadUrls) {
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}/${data[2]}`;
downloadUrl = getPublicUrl(`files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}/${data[2]}`);
} else {
downloadUrl =`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${data[0]}`;
downloadUrl = getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${data[0]}`);
}
actions.push({

View file

@ -37,7 +37,8 @@ import {
} from '../../../../shared/imports';
import axios from "../../lib/axios";
import {getUrl} from "../../lib/urls";
import styles from "../styles.scss";
import listStyles from "../styles.scss";
import styles from "../../lib/styles.scss";
function truncate(str, len, ending = '...') {
@ -372,7 +373,7 @@ export default class CUD extends Component {
mappingSettings = (
<div>
{settingsRows}
<Fieldset label={t('Mapping')} className={styles.mapping}>
<Fieldset label={t('Mapping')} className={listStyles.mapping}>
{mappingRows}
</Fieldset>
</div>

View file

@ -156,7 +156,8 @@ export default class List extends Component {
<div>
{tableDeleteDialogRender(this, `rest/subscriptions/${this.props.list.id}`, t('Deleting subscription ...'), t('Subscription deleted'))}
<Toolbar>
<a href={getPublicUrl(`subscription/${this.props.list.cid}`)} className="btn-default"><Button label={t('Subscription Form')} className="btn-default"/></a>
<a href={getPublicUrl(`subscription/${this.props.list.cid}`)}><Button label={t('Subscription Form')} className="btn-default"/></a>
<a href={getUrl(`subscriptions/export/${this.props.list.id}/`+ (this.props.segmentId || 0))}><Button label={t('Export as CSV')} className="btn-primary"/></a>
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
</Toolbar>

View file

@ -22,7 +22,7 @@ export default class Output extends Component {
@withAsyncErrorHandler
async loadOutput() {
const id = parseInt(this.props.match.params.id);
const id = parseInt(this.props.match.params.reportId);
const outputRespPromise = axios.get(getUrl(`rest/report-output/${id}`));
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
const [outputResp, reportResp] = await Promise.all([outputRespPromise, reportRespPromise]);

View file

@ -23,7 +23,7 @@ export default class View extends Component {
@withAsyncErrorHandler
async loadContent() {
const id = parseInt(this.props.match.params.id);
const id = parseInt(this.props.match.params.reportId);
const contentRespPromise = axios.get(getUrl(`rest/report-content/${id}`));
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
const [contentResp, reportResp] = await Promise.all([contentRespPromise, reportRespPromise]);

View file

@ -509,6 +509,69 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
</tbody>
</table>
<Trans><p>For RSS campaigns, the following further tags can be used.</p></Trans>
<table className="table table-bordered table-condensed table-striped">
<thead>
<tr>
<th>
<Trans>Merge tag</Trans>
</th>
<th>
<Trans>Description</Trans>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
[RSS_ENTRY_TITLE]
</th>
<td>
<Trans>RSS entry title</Trans>
</td>
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_DATE]
</th>
<td>
<Trans>RSS entry date</Trans>
</td>
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_LINK]
</th>
<td>
<Trans>RSS entry link</Trans>
</td>
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_CONTENT]
</th>
<td>
<Trans>Content of an RSS entry</Trans>
</td>
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_SUMMARY]
</th>
<td>
<Trans>RSS entry summary</Trans>
</td>
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_IMAGE_URL]
</th>
<td>
<Trans>RSS entry image URL</Trans>
</td>
</tr>
</tbody>
</table>
</div>}
</AlignedRow>