diff --git a/app.js b/app.js
index e1191b34..d500258f 100644
--- a/app.js
+++ b/app.js
@@ -36,7 +36,6 @@ const webhooks = require('./routes/webhooks');
const subscription = require('./routes/subscription');
const archive = require('./routes/archive');
const api = require('./routes/api');
-const blacklist = require('./routes/blacklist');
const editorapi = require('./routes/editorapi');
const grapejs = require('./routes/grapejs');
const mosaico = require('./routes/mosaico');
@@ -56,12 +55,14 @@ const fieldsRest = require('./routes/rest/fields');
const sharesRest = require('./routes/rest/shares');
const segmentsRest = require('./routes/rest/segments');
const subscriptionsRest = require('./routes/rest/subscriptions');
+const blacklistRest = require('./routes/rest/blacklist');
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
const usersLegacyIntegration = require('./routes/users-legacy-integration');
const accountLegacyIntegration = require('./routes/account-legacy-integration');
const reportsLegacyIntegration = require('./routes/reports-legacy-integration');
const listsLegacyIntegration = require('./routes/lists-legacy-integration');
+const blacklistLegacyIntegration = require('./routes/blacklist-legacy-integration');
const interoperableErrors = require('./shared/interoperable-errors');
@@ -235,7 +236,6 @@ app.use('/lists', lists);
app.use('/templates', templates);
app.use('/campaigns', campaigns);
app.use('/settings', settings);
-app.use('/blacklist', blacklist);
app.use('/links', links);
app.use('/fields', fields);
app.use('/forms', forms);
@@ -259,6 +259,7 @@ app.use('/users', usersLegacyIntegration);
app.use('/namespaces', namespacesLegacyIntegration);
app.use('/account', accountLegacyIntegration);
app.use('/lists', listsLegacyIntegration);
+app.use('/blacklist', blacklistLegacyIntegration);
if (config.reports && config.reports.enabled === true) {
app.use('/reports', reports);
@@ -281,6 +282,7 @@ app.use('/rest', fieldsRest);
app.use('/rest', sharesRest);
app.use('/rest', segmentsRest);
app.use('/rest', subscriptionsRest);
+app.use('/rest', blacklistRest);
if (config.reports && config.reports.enabled === true) {
app.use('/rest', reportTemplatesRest);
diff --git a/client/package.json b/client/package.json
index b174dba0..ba13c064 100644
--- a/client/package.json
+++ b/client/package.json
@@ -35,6 +35,8 @@
"react-router-dom": "^4.1.1",
"react-sortable-tree": "^1.2.0",
"slugify": "^1.1.0",
+ "react-dnd-html5-backend": "^2.4.1",
+ "react-dnd-touch-backend": "^0.3.13",
"url-parse": "^1.1.9"
},
"devDependencies": {
@@ -48,8 +50,6 @@
"css-loader": "^0.28.4",
"i18next-conv": "^3.0.3",
"node-sass": "^4.5.3",
- "react-dnd-html5-backend": "^2.4.1",
- "react-dnd-touch-backend": "^0.3.13",
"sass-loader": "^6.0.6",
"style-loader": "^0.18.2",
"url-loader": "^0.5.9",
diff --git a/client/src/account/root.js b/client/src/account/root.js
index 9e163bf6..f479f8a9 100644
--- a/client/src/account/root.js
+++ b/client/src/account/root.js
@@ -19,28 +19,28 @@ const getStructure = t => {
login: {
title: t('Sign in'),
link: '/account/login',
- component: Login,
+ panelComponent: Login,
},
api: {
title: t('API'),
link: '/account/api',
- component: API
+ panelComponent: API
}
};
if (mailtrainConfig.isAuthMethodLocal) {
subPaths.forgot = {
title: t('Password reset'),
- extraParams: [':username?'],
- link: '/account/forgot',
- component: Reset
+ extraParams: [':username?'],
+ link: '/account/forgot',
+ panelComponent: Reset
};
subPaths.reset = {
title: t('Password reset'),
- extraParams: [':username', ':resetToken'],
- link: '/account/reset',
- component: ResetLink
+ extraParams: [':username', ':resetToken'],
+ link: '/account/reset',
+ panelComponent: ResetLink
};
}
@@ -52,10 +52,9 @@ const getStructure = t => {
account: {
title: t('Account'),
link: '/account',
- component: Account,
+ panelComponent: Account,
children: subPaths
-
}
}
}
@@ -67,6 +66,6 @@ export default function() {
,
document.getElementById('root')
);
-};
+}
diff --git a/client/src/blacklist/List.js b/client/src/blacklist/List.js
new file mode 100644
index 00000000..cd9751fc
--- /dev/null
+++ b/client/src/blacklist/List.js
@@ -0,0 +1,129 @@
+'use strict';
+
+import React, {Component} from "react";
+import {translate} from "react-i18next";
+import {requiresAuthenticatedUser, Title, withPageHelpers} from "../lib/page";
+import {withAsyncErrorHandler, withErrorHandling} from "../lib/error-handling";
+import {Table} from "../lib/table";
+import {ButtonRow, Form, InputField, withForm, FormSendMethod} from "../lib/form";
+import {Button, Icon} from "../lib/bootstrap-components";
+import axios from "../lib/axios";
+
+@translate()
+@withForm
+@withPageHelpers
+@withErrorHandling
+@requiresAuthenticatedUser
+export default class List extends Component {
+ constructor(props) {
+ super(props);
+
+ const t = props.t;
+
+ this.state = {};
+
+ this.initForm({
+ serverValidation: {
+ url: '/rest/blacklist-validate',
+ changed: ['email']
+ }
+ });
+ }
+
+ static propTypes = {
+ }
+
+ clearFields() {
+ this.populateFormValues({
+ email: ''
+ });
+ }
+
+ localValidateFormValues(state) {
+ const t = this.props.t;
+
+ const email = state.getIn(['email', 'value']);
+ const emailServerValidation = state.getIn(['email', 'serverValidation']);
+
+ if (!email) {
+ state.setIn(['email', 'error'], t('Email must not be empty'));
+ } else if (emailServerValidation && emailServerValidation.invalid) {
+ state.setIn(['email', 'error'], t('Invalid email address.'));
+ } else if (emailServerValidation && emailServerValidation.exists) {
+ state.setIn(['email', 'error'], t('The email is already on blacklist.'));
+ } else if (!emailServerValidation) {
+ state.setIn(['email', 'error'], t('Validation is in progress...'));
+ } else {
+ state.setIn(['email', 'error'], null);
+ }
+ }
+
+ async submitHandler() {
+ const t = this.props.t;
+
+ this.disableForm();
+ this.setFormStatusMessage('info', t('Saving ...'));
+
+ const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/blacklist');
+
+ if (submitSuccessful) {
+ this.hideFormValidation();
+ this.clearFields();
+ this.enableForm();
+
+ this.clearFormStatusMessage();
+ this.blacklistTable.refresh();
+
+ } else {
+ this.enableForm();
+ this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and try again.'));
+ }
+ }
+
+ componentDidMount() {
+ this.clearFields();
+ }
+
+ @withAsyncErrorHandler
+ async deleteBlacklisted(email) {
+ await axios.delete(`/rest/blacklist/${email}`);
+ this.blacklistTable.refresh();
+ }
+
+ render() {
+ const t = this.props.t;
+
+ const columns = [
+ { data: 0, title: t('Email') },
+ {
+ actions: data => [
+ {
+ label: ,
+ action: () => this.deleteBlacklisted(data[0])
+ }
+ ]
+ }
+ ];
+
+ return (
+
+
{t('Blacklist')}
+
+
{t('Add Email to Blacklist')}
+
+
+
+
+
{t('Blacklisted Emails')}
+
+
this.blacklistTable = node} withHeader dataUrl="/rest/blacklist-table" columns={columns} />
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/client/src/blacklist/root.js b/client/src/blacklist/root.js
new file mode 100644
index 00000000..910a05aa
--- /dev/null
+++ b/client/src/blacklist/root.js
@@ -0,0 +1,34 @@
+'use strict';
+
+import React from "react";
+import ReactDOM from "react-dom";
+import {I18nextProvider} from "react-i18next";
+import i18n from "../lib/i18n";
+
+import {Section} from "../lib/page";
+import List from "./List";
+
+const getStructure = t => {
+ return {
+ '': {
+ title: t('Home'),
+ externalLink: '/',
+ children: {
+ 'blacklist': {
+ title: t('Blacklist'),
+ link: '/blacklist',
+ panelComponent: List,
+ }
+ }
+ }
+ }
+};
+
+export default function() {
+ ReactDOM.render(
+ ,
+ document.getElementById('root')
+ );
+}
+
+
diff --git a/client/src/lib/bootstrap-components.js b/client/src/lib/bootstrap-components.js
index fecd7f26..946d0499 100644
--- a/client/src/lib/bootstrap-components.js
+++ b/client/src/lib/bootstrap-components.js
@@ -35,14 +35,19 @@ class DismissibleAlert extends Component {
class Icon extends Component {
static propTypes = {
icon: PropTypes.string.isRequired,
+ family: PropTypes.string,
title: PropTypes.string,
className: PropTypes.string
}
+ static defaultProps = {
+ family: 'glyphicon'
+ }
+
render() {
const props = this.props;
- return ;
+ return ;
}
}
@@ -129,6 +134,7 @@ class ActionLink extends Component {
async onClick(evt) {
if (this.props.onClickAsync) {
evt.preventDefault();
+ evt.stopPropagation();
await this.props.onClickAsync(evt);
}
diff --git a/client/src/lib/form.js b/client/src/lib/form.js
index 76f304ea..349da8f5 100644
--- a/client/src/lib/form.js
+++ b/client/src/lib/form.js
@@ -10,13 +10,10 @@ import { withPageHelpers } from './page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import { TreeTable, TreeSelectMode } from './tree';
import { Table, TableSelectMode } from './table';
-import { Button } from "./bootstrap-components";
+import {Button, Icon} from "./bootstrap-components";
import brace from 'brace';
import AceEditor from 'react-ace';
-import 'brace/mode/javascript';
-import 'brace/mode/json';
-import 'brace/mode/handlebars';
import 'brace/theme/github';
import DayPicker from 'react-day-picker';
@@ -42,7 +39,8 @@ class Form extends Component {
static propTypes = {
stateOwner: PropTypes.object.isRequired,
onSubmitAsync: PropTypes.func,
- format: PropTypes.string
+ format: PropTypes.string,
+ noStatus: PropTypes.bool
}
static childContextTypes = {
@@ -60,7 +58,7 @@ class Form extends Component {
const t = this.props.t;
const owner = this.props.stateOwner;
-
+
evt.preventDefault();
if (this.props.onSubmitAsync) {
@@ -94,10 +92,10 @@ class Form extends Component {
- {statusMessageText &&
-
- {statusMessageText}
-
+ {!props.noStatus && statusMessageText &&
+
+ {statusMessageText}
+
}
);
@@ -107,18 +105,46 @@ class Form extends Component {
class Fieldset extends Component {
static propTypes = {
- label: PropTypes.string
+ id: PropTypes.string,
+ label: PropTypes.string,
+ help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ }
+
+ static contextTypes = {
+ formStateOwner: PropTypes.object.isRequired
}
render() {
const props = this.props;
+ const owner = this.context.formStateOwner;
+ const id = this.props.id;
+ const htmlId = 'form_' + id;
+
+ const className = id ? owner.addFormValidationClass('', id) : '';
+
+ let helpBlock = null;
+ if (this.props.help) {
+ helpBlock = {this.props.help}
;
+ }
+
+ let validationBlock = null;
+ if (id) {
+ const validationMsg = id && owner.getFormValidationMessage(id);
+ if (validationMsg) {
+ validationBlock = {validationMsg}
;
+ }
+ }
return (
-