Blacklist functionality
Some API improvements
This commit is contained in:
parent
c343e4efd3
commit
9203b5cee7
40 changed files with 726 additions and 398 deletions
6
app.js
6
app.js
|
@ -36,7 +36,6 @@ const webhooks = require('./routes/webhooks');
|
||||||
const subscription = require('./routes/subscription');
|
const subscription = require('./routes/subscription');
|
||||||
const archive = require('./routes/archive');
|
const archive = require('./routes/archive');
|
||||||
const api = require('./routes/api');
|
const api = require('./routes/api');
|
||||||
const blacklist = require('./routes/blacklist');
|
|
||||||
const editorapi = require('./routes/editorapi');
|
const editorapi = require('./routes/editorapi');
|
||||||
const grapejs = require('./routes/grapejs');
|
const grapejs = require('./routes/grapejs');
|
||||||
const mosaico = require('./routes/mosaico');
|
const mosaico = require('./routes/mosaico');
|
||||||
|
@ -56,12 +55,14 @@ const fieldsRest = require('./routes/rest/fields');
|
||||||
const sharesRest = require('./routes/rest/shares');
|
const sharesRest = require('./routes/rest/shares');
|
||||||
const segmentsRest = require('./routes/rest/segments');
|
const segmentsRest = require('./routes/rest/segments');
|
||||||
const subscriptionsRest = require('./routes/rest/subscriptions');
|
const subscriptionsRest = require('./routes/rest/subscriptions');
|
||||||
|
const blacklistRest = require('./routes/rest/blacklist');
|
||||||
|
|
||||||
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
||||||
const usersLegacyIntegration = require('./routes/users-legacy-integration');
|
const usersLegacyIntegration = require('./routes/users-legacy-integration');
|
||||||
const accountLegacyIntegration = require('./routes/account-legacy-integration');
|
const accountLegacyIntegration = require('./routes/account-legacy-integration');
|
||||||
const reportsLegacyIntegration = require('./routes/reports-legacy-integration');
|
const reportsLegacyIntegration = require('./routes/reports-legacy-integration');
|
||||||
const listsLegacyIntegration = require('./routes/lists-legacy-integration');
|
const listsLegacyIntegration = require('./routes/lists-legacy-integration');
|
||||||
|
const blacklistLegacyIntegration = require('./routes/blacklist-legacy-integration');
|
||||||
|
|
||||||
const interoperableErrors = require('./shared/interoperable-errors');
|
const interoperableErrors = require('./shared/interoperable-errors');
|
||||||
|
|
||||||
|
@ -235,7 +236,6 @@ app.use('/lists', lists);
|
||||||
app.use('/templates', templates);
|
app.use('/templates', templates);
|
||||||
app.use('/campaigns', campaigns);
|
app.use('/campaigns', campaigns);
|
||||||
app.use('/settings', settings);
|
app.use('/settings', settings);
|
||||||
app.use('/blacklist', blacklist);
|
|
||||||
app.use('/links', links);
|
app.use('/links', links);
|
||||||
app.use('/fields', fields);
|
app.use('/fields', fields);
|
||||||
app.use('/forms', forms);
|
app.use('/forms', forms);
|
||||||
|
@ -259,6 +259,7 @@ app.use('/users', usersLegacyIntegration);
|
||||||
app.use('/namespaces', namespacesLegacyIntegration);
|
app.use('/namespaces', namespacesLegacyIntegration);
|
||||||
app.use('/account', accountLegacyIntegration);
|
app.use('/account', accountLegacyIntegration);
|
||||||
app.use('/lists', listsLegacyIntegration);
|
app.use('/lists', listsLegacyIntegration);
|
||||||
|
app.use('/blacklist', blacklistLegacyIntegration);
|
||||||
|
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
app.use('/reports', reports);
|
app.use('/reports', reports);
|
||||||
|
@ -281,6 +282,7 @@ app.use('/rest', fieldsRest);
|
||||||
app.use('/rest', sharesRest);
|
app.use('/rest', sharesRest);
|
||||||
app.use('/rest', segmentsRest);
|
app.use('/rest', segmentsRest);
|
||||||
app.use('/rest', subscriptionsRest);
|
app.use('/rest', subscriptionsRest);
|
||||||
|
app.use('/rest', blacklistRest);
|
||||||
|
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
app.use('/rest', reportTemplatesRest);
|
app.use('/rest', reportTemplatesRest);
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
"react-sortable-tree": "^1.2.0",
|
"react-sortable-tree": "^1.2.0",
|
||||||
"slugify": "^1.1.0",
|
"slugify": "^1.1.0",
|
||||||
|
"react-dnd-html5-backend": "^2.4.1",
|
||||||
|
"react-dnd-touch-backend": "^0.3.13",
|
||||||
"url-parse": "^1.1.9"
|
"url-parse": "^1.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -48,8 +50,6 @@
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.4",
|
||||||
"i18next-conv": "^3.0.3",
|
"i18next-conv": "^3.0.3",
|
||||||
"node-sass": "^4.5.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",
|
"sass-loader": "^6.0.6",
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"url-loader": "^0.5.9",
|
"url-loader": "^0.5.9",
|
||||||
|
|
|
@ -19,12 +19,12 @@ const getStructure = t => {
|
||||||
login: {
|
login: {
|
||||||
title: t('Sign in'),
|
title: t('Sign in'),
|
||||||
link: '/account/login',
|
link: '/account/login',
|
||||||
component: Login,
|
panelComponent: Login,
|
||||||
},
|
},
|
||||||
api: {
|
api: {
|
||||||
title: t('API'),
|
title: t('API'),
|
||||||
link: '/account/api',
|
link: '/account/api',
|
||||||
component: API
|
panelComponent: API
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,14 +33,14 @@ const getStructure = t => {
|
||||||
title: t('Password reset'),
|
title: t('Password reset'),
|
||||||
extraParams: [':username?'],
|
extraParams: [':username?'],
|
||||||
link: '/account/forgot',
|
link: '/account/forgot',
|
||||||
component: Reset
|
panelComponent: Reset
|
||||||
};
|
};
|
||||||
|
|
||||||
subPaths.reset = {
|
subPaths.reset = {
|
||||||
title: t('Password reset'),
|
title: t('Password reset'),
|
||||||
extraParams: [':username', ':resetToken'],
|
extraParams: [':username', ':resetToken'],
|
||||||
link: '/account/reset',
|
link: '/account/reset',
|
||||||
component: ResetLink
|
panelComponent: ResetLink
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,10 +52,9 @@ const getStructure = t => {
|
||||||
account: {
|
account: {
|
||||||
title: t('Account'),
|
title: t('Account'),
|
||||||
link: '/account',
|
link: '/account',
|
||||||
component: Account,
|
panelComponent: Account,
|
||||||
|
|
||||||
children: subPaths
|
children: subPaths
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +66,6 @@ export default function() {
|
||||||
<I18nextProvider i18n={ i18n }><Section root='/account/login' structure={getStructure}/></I18nextProvider>,
|
<I18nextProvider i18n={ i18n }><Section root='/account/login' structure={getStructure}/></I18nextProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
129
client/src/blacklist/List.js
Normal file
129
client/src/blacklist/List.js
Normal file
|
@ -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: <Icon icon="remove" title={t('Remove from blacklist')}/>,
|
||||||
|
action: () => this.deleteBlacklisted(data[0])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title>{t('Blacklist')}</Title>
|
||||||
|
|
||||||
|
<h3 className="legend">{t('Add Email to Blacklist')}</h3>
|
||||||
|
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||||
|
<InputField id="email" label={t('Email')}/>
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Add to Blacklist')}/>
|
||||||
|
</ButtonRow>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h3 className="legend">{t('Blacklisted Emails')}</h3>
|
||||||
|
|
||||||
|
<Table ref={node => this.blacklistTable = node} withHeader dataUrl="/rest/blacklist-table" columns={columns} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
34
client/src/blacklist/root.js
Normal file
34
client/src/blacklist/root.js
Normal file
|
@ -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(
|
||||||
|
<I18nextProvider i18n={ i18n }><Section root='/blacklist' structure={getStructure}/></I18nextProvider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
8
client/src/lib/bootstrap-components.js
vendored
8
client/src/lib/bootstrap-components.js
vendored
|
@ -35,14 +35,19 @@ class DismissibleAlert extends Component {
|
||||||
class Icon extends Component {
|
class Icon extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
|
family: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
className: PropTypes.string
|
className: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
family: 'glyphicon'
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const props = this.props;
|
const props = this.props;
|
||||||
|
|
||||||
return <span className={'glyphicon glyphicon-' + props.icon + (props.className ? ' ' + props.className : '')} title={props.title}></span>;
|
return <span className={`${props.family} ${props.family}-${props.icon}` + (props.className ? ' ' + props.className : '')} title={props.title}></span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +134,7 @@ class ActionLink extends Component {
|
||||||
async onClick(evt) {
|
async onClick(evt) {
|
||||||
if (this.props.onClickAsync) {
|
if (this.props.onClickAsync) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
await this.props.onClickAsync(evt);
|
await this.props.onClickAsync(evt);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,10 @@ import { withPageHelpers } from './page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
import { TreeTable, TreeSelectMode } from './tree';
|
import { TreeTable, TreeSelectMode } from './tree';
|
||||||
import { Table, TableSelectMode } from './table';
|
import { Table, TableSelectMode } from './table';
|
||||||
import { Button } from "./bootstrap-components";
|
import {Button, Icon} from "./bootstrap-components";
|
||||||
|
|
||||||
import brace from 'brace';
|
import brace from 'brace';
|
||||||
import AceEditor from 'react-ace';
|
import AceEditor from 'react-ace';
|
||||||
import 'brace/mode/javascript';
|
|
||||||
import 'brace/mode/json';
|
|
||||||
import 'brace/mode/handlebars';
|
|
||||||
import 'brace/theme/github';
|
import 'brace/theme/github';
|
||||||
|
|
||||||
import DayPicker from 'react-day-picker';
|
import DayPicker from 'react-day-picker';
|
||||||
|
@ -42,7 +39,8 @@ class Form extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
stateOwner: PropTypes.object.isRequired,
|
stateOwner: PropTypes.object.isRequired,
|
||||||
onSubmitAsync: PropTypes.func,
|
onSubmitAsync: PropTypes.func,
|
||||||
format: PropTypes.string
|
format: PropTypes.string,
|
||||||
|
noStatus: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
|
@ -94,7 +92,7 @@ class Form extends Component {
|
||||||
<fieldset disabled={owner.isFormDisabled()}>
|
<fieldset disabled={owner.isFormDisabled()}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{statusMessageText &&
|
{!props.noStatus && statusMessageText &&
|
||||||
<AlignedRow htmlId="form-status-message">
|
<AlignedRow htmlId="form-status-message">
|
||||||
<p className={`alert alert-${statusMessageSeverity} ${styles.formStatus}`} role="alert">{statusMessageText}</p>
|
<p className={`alert alert-${statusMessageSeverity} ${styles.formStatus}`} role="alert">{statusMessageText}</p>
|
||||||
</AlignedRow>
|
</AlignedRow>
|
||||||
|
@ -107,16 +105,44 @@ class Form extends Component {
|
||||||
|
|
||||||
class Fieldset extends Component {
|
class Fieldset extends Component {
|
||||||
static propTypes = {
|
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() {
|
render() {
|
||||||
const props = this.props;
|
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 = <div className="help-block" id={htmlId + '_help'}>{this.props.help}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validationBlock = null;
|
||||||
|
if (id) {
|
||||||
|
const validationMsg = id && owner.getFormValidationMessage(id);
|
||||||
|
if (validationMsg) {
|
||||||
|
validationBlock = <div className="help-block" id={htmlId + '_help_validation'}>{validationMsg}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset className={className}>
|
||||||
{props.label ? <legend>{props.label}</legend> : null}
|
{props.label ? <legend>{props.label}</legend> : null}
|
||||||
|
<div className="fieldset-content">
|
||||||
{props.children}
|
{props.children}
|
||||||
|
{helpBlock}
|
||||||
|
{validationBlock}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -411,6 +437,7 @@ class TextArea extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@translate()
|
||||||
class DatePicker extends Component {
|
class DatePicker extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -426,7 +453,9 @@ class DatePicker extends Component {
|
||||||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||||
format: PropTypes.string,
|
format: PropTypes.string,
|
||||||
birthday: PropTypes.bool,
|
birthday: PropTypes.bool,
|
||||||
dateFormat: PropTypes.string
|
dateFormat: PropTypes.string,
|
||||||
|
formatDate: PropTypes.func,
|
||||||
|
parseDate: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -447,7 +476,12 @@ class DatePicker extends Component {
|
||||||
const owner = this.context.formStateOwner;
|
const owner = this.context.formStateOwner;
|
||||||
const id = this.props.id;
|
const id = this.props.id;
|
||||||
const props = this.props;
|
const props = this.props;
|
||||||
|
|
||||||
|
if (props.formatDate) {
|
||||||
|
owner.updateFormValue(id, props.formatDate(date));
|
||||||
|
} else {
|
||||||
owner.updateFormValue(id, props.birthday ? formatBirthday(props.dateFormat, date) : formatDate(props.dateFormat, date));
|
owner.updateFormValue(id, props.birthday ? formatBirthday(props.dateFormat, date) : formatDate(props.dateFormat, date));
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
opened: false
|
opened: false
|
||||||
|
@ -459,6 +493,7 @@ class DatePicker extends Component {
|
||||||
const owner = this.context.formStateOwner;
|
const owner = this.context.formStateOwner;
|
||||||
const id = this.props.id;
|
const id = this.props.id;
|
||||||
const htmlId = 'form_' + id;
|
const htmlId = 'form_' + id;
|
||||||
|
const t = props.t;
|
||||||
|
|
||||||
function BirthdayPickerCaption({ date, localeUtils, onChange }) {
|
function BirthdayPickerCaption({ date, localeUtils, onChange }) {
|
||||||
const months = localeUtils.getMonths();
|
const months = localeUtils.getMonths();
|
||||||
|
@ -472,7 +507,15 @@ class DatePicker extends Component {
|
||||||
let selectedDate, captionElement, fromMonth, toMonth, placeholder;
|
let selectedDate, captionElement, fromMonth, toMonth, placeholder;
|
||||||
const selectedDateStr = owner.getFormValue(id) || '';
|
const selectedDateStr = owner.getFormValue(id) || '';
|
||||||
if (props.birthday) {
|
if (props.birthday) {
|
||||||
|
if (props.parseDate) {
|
||||||
|
selectedDate = props.parseDate(selectedDateStr);
|
||||||
|
if (selectedDate) {
|
||||||
|
selectedDate = moment(selectedDate).set('year', birthdayYear).toDate();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
selectedDate = parseBirthday(props.dateFormat, selectedDateStr);
|
selectedDate = parseBirthday(props.dateFormat, selectedDateStr);
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedDate) {
|
if (!selectedDate) {
|
||||||
selectedDate = moment().set('year', birthdayYear).toDate();
|
selectedDate = moment().set('year', birthdayYear).toDate();
|
||||||
}
|
}
|
||||||
|
@ -482,8 +525,13 @@ class DatePicker extends Component {
|
||||||
toMonth = new Date(birthdayYear, 11, 31);
|
toMonth = new Date(birthdayYear, 11, 31);
|
||||||
placeholder = getBirthdayFormatString(props.dateFormat);
|
placeholder = getBirthdayFormatString(props.dateFormat);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (props.parseDate) {
|
||||||
|
selectedDate = props.parseDate(selectedDateStr);
|
||||||
} else {
|
} else {
|
||||||
selectedDate = parseDate(props.dateFormat, selectedDateStr);
|
selectedDate = parseDate(props.dateFormat, selectedDateStr);
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedDate) {
|
if (!selectedDate) {
|
||||||
selectedDate = moment().toDate();
|
selectedDate = moment().toDate();
|
||||||
}
|
}
|
||||||
|
@ -495,7 +543,7 @@ class DatePicker extends Component {
|
||||||
<div>
|
<div>
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<input type="text" value={selectedDateStr} placeholder={placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
|
<input type="text" value={selectedDateStr} placeholder={placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
|
||||||
<span className="input-group-addon" onClick={::this.toggleDayPicker}><span className="glyphicon glyphicon-th"></span></span>
|
<span className="input-group-addon" onClick={::this.toggleDayPicker}><Icon icon="calendar" title={t('Open calendar')}/></span>
|
||||||
</div>
|
</div>
|
||||||
{this.state.opened &&
|
{this.state.opened &&
|
||||||
<div className={styles.dayPickerWrapper}>
|
<div className={styles.dayPickerWrapper}>
|
||||||
|
@ -729,7 +777,7 @@ class TableSelect extends Component {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.tableSelectTable + (this.state.open ? '' : ' ' + styles.tableSelectTableHidden)}>
|
<div className={styles.tableSelectTable + (this.state.open ? '' : ' ' + styles.tableSelectTableHidden)}>
|
||||||
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selectionKeyIndex={props.selectionKeyIndex} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -737,7 +785,7 @@ class TableSelect extends Component {
|
||||||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selectionKeyIndex={props.selectionKeyIndex} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -938,6 +986,7 @@ function withForm(target) {
|
||||||
delete data.hash;
|
delete data.hash;
|
||||||
|
|
||||||
if (mutator) {
|
if (mutator) {
|
||||||
|
// FIXME - change the interface such that if the mutator is provided, it is supposed to return which fields to keep in the form
|
||||||
mutator(data);
|
mutator(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -951,6 +1000,7 @@ function withForm(target) {
|
||||||
const data = this.getFormValues();
|
const data = this.getFormValues();
|
||||||
|
|
||||||
if (mutator) {
|
if (mutator) {
|
||||||
|
// FIXME - change the interface such that the mutator is supposed to create the object to be submitted
|
||||||
mutator(data);
|
mutator(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1152,6 +1202,17 @@ function withForm(target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof interoperableErrors.NamespaceNotFoundError) {
|
||||||
|
this.disableForm();
|
||||||
|
this.setFormStatusMessage('danger',
|
||||||
|
<span>
|
||||||
|
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||||
|
{t('It seems that someone else has deleted the target namespace in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof interoperableErrors.NotFoundError) {
|
if (error instanceof interoperableErrors.NotFoundError) {
|
||||||
this.disableForm();
|
this.disableForm();
|
||||||
this.setFormStatusMessage('danger',
|
this.setFormStatusMessage('danger',
|
||||||
|
|
156
client/src/lib/page-common.js
Normal file
156
client/src/lib/page-common.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {withRouter} from "react-router";
|
||||||
|
import {withErrorHandling} from "./error-handling";
|
||||||
|
import axios from "../lib/axios";
|
||||||
|
|
||||||
|
function needsResolve(route, nextRoute, match, nextMatch) {
|
||||||
|
const resolve = route.resolve;
|
||||||
|
const nextResolve = nextRoute.resolve;
|
||||||
|
|
||||||
|
if (Object.keys(resolve).length === Object.keys(nextResolve).length) {
|
||||||
|
for (const key in resolve) {
|
||||||
|
if (!(key in nextResolve) ||
|
||||||
|
resolve[key](match.params) !== nextResolve[key](nextMatch.params)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolve(route, match) {
|
||||||
|
const keys = Object.keys(route.resolve);
|
||||||
|
|
||||||
|
const promises = keys.map(key => {
|
||||||
|
const url = route.resolve[key](match.params);
|
||||||
|
if (url) {
|
||||||
|
return axios.get(url);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({data: null});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const resolvedArr = await Promise.all(promises);
|
||||||
|
|
||||||
|
const resolved = {};
|
||||||
|
for (let idx = 0; idx < keys.length; idx++) {
|
||||||
|
resolved[keys[idx]] = resolvedArr[idx].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
|
||||||
|
let routes = [];
|
||||||
|
for (let routeKey in structure) {
|
||||||
|
const entry = structure[routeKey];
|
||||||
|
|
||||||
|
let path = urlPrefix + routeKey;
|
||||||
|
let pathWithParams = path;
|
||||||
|
|
||||||
|
if (entry.extraParams) {
|
||||||
|
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
let entryResolve;
|
||||||
|
if (entry.resolve) {
|
||||||
|
entryResolve = Object.assign({}, resolve, entry.resolve);
|
||||||
|
} else {
|
||||||
|
entryResolve = resolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
let navKeys;
|
||||||
|
const entryNavs = [];
|
||||||
|
if (entry.navs) {
|
||||||
|
navKeys = Object.keys(entry.navs);
|
||||||
|
|
||||||
|
for (const navKey of navKeys) {
|
||||||
|
const nav = entry.navs[navKey];
|
||||||
|
|
||||||
|
entryNavs.push({
|
||||||
|
title: nav.title,
|
||||||
|
visible: nav.visible,
|
||||||
|
link: nav.link,
|
||||||
|
externalLink: nav.externalLink
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = {
|
||||||
|
path: (pathWithParams === '' ? '/' : pathWithParams),
|
||||||
|
panelComponent: entry.panelComponent,
|
||||||
|
panelRender: entry.panelRender,
|
||||||
|
primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
|
||||||
|
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
|
||||||
|
title: entry.title,
|
||||||
|
link: entry.link,
|
||||||
|
resolve: entryResolve,
|
||||||
|
parents,
|
||||||
|
navs: [...navs, ...entryNavs]
|
||||||
|
};
|
||||||
|
|
||||||
|
routes.push(route);
|
||||||
|
|
||||||
|
const childrenParents = [...parents, route];
|
||||||
|
|
||||||
|
if (entry.navs) {
|
||||||
|
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
|
||||||
|
const navKey = navKeys[navKeyIdx];
|
||||||
|
const nav = entry.navs[navKey];
|
||||||
|
|
||||||
|
const childNavs = [...entryNavs];
|
||||||
|
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
|
||||||
|
|
||||||
|
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.children) {
|
||||||
|
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withPageHelpers(target) {
|
||||||
|
target = withErrorHandling(target);
|
||||||
|
|
||||||
|
const inst = target.prototype;
|
||||||
|
|
||||||
|
const contextTypes = target.contextTypes || {};
|
||||||
|
|
||||||
|
contextTypes.sectionContent = PropTypes.object.isRequired;
|
||||||
|
|
||||||
|
target.contextTypes = contextTypes;
|
||||||
|
|
||||||
|
inst.setFlashMessage = function(severity, text) {
|
||||||
|
return this.context.sectionContent.setFlashMessage(severity, text);
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.navigateTo = function(path) {
|
||||||
|
return this.context.sectionContent.navigateTo(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.navigateBack = function() {
|
||||||
|
return this.context.sectionContent.navigateBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.navigateToWithFlashMessage = function(path, severity, text) {
|
||||||
|
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
needsResolve,
|
||||||
|
resolve,
|
||||||
|
getRoutes,
|
||||||
|
withPageHelpers
|
||||||
|
};
|
|
@ -1,17 +1,16 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, {Component} from "react";
|
||||||
import { translate } from 'react-i18next';
|
import {translate} from "react-i18next";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { withRouter } from 'react-router';
|
import {withRouter} from "react-router";
|
||||||
import {BrowserRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom'
|
import {BrowserRouter as Router, Link, Redirect, Route, Switch} from "react-router-dom";
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import {withAsyncErrorHandler, withErrorHandling} from "./error-handling";
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from "../../../shared/interoperable-errors";
|
||||||
import { DismissibleAlert, Button } from './bootstrap-components';
|
import {Button, DismissibleAlert} from "./bootstrap-components";
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from "mailtrainConfig";
|
||||||
import axios from '../lib/axios';
|
|
||||||
import styles from "./styles.scss";
|
import styles from "./styles.scss";
|
||||||
|
import {getRoutes, needsResolve, resolve, withPageHelpers} from "./page-common";
|
||||||
|
|
||||||
class Breadcrumb extends Component {
|
class Breadcrumb extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -163,28 +162,42 @@ class RouteContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async resolve() {
|
async resolve(props) {
|
||||||
const route = this.props.route;
|
if (Object.keys(props.route.resolve).length === 0) {
|
||||||
|
this.setState({
|
||||||
|
resolved: {}
|
||||||
|
});
|
||||||
|
|
||||||
const keys = Object.keys(route.resolve);
|
} else {
|
||||||
|
this.setState({
|
||||||
|
resolved: null
|
||||||
|
});
|
||||||
|
|
||||||
if (keys.length > 0) {
|
const resolved = await resolve(props.route, props.match);
|
||||||
const promises = keys.map(key => axios.get(route.resolve[key](this.props.match.params)));
|
|
||||||
const resolvedArr = await Promise.all(promises);
|
|
||||||
|
|
||||||
const resolved = {};
|
|
||||||
for (let idx = 0; idx < keys.length; idx++) {
|
|
||||||
resolved[keys[idx]] = resolvedArr[idx].data;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!this.disregardResolve) { // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
|
||||||
this.setState({
|
this.setState({
|
||||||
resolved
|
resolved
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.resolve();
|
this.resolve(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.props.match.params !== nextProps.match.params && needsResolve(this.props.route, nextProps.route, this.props.match, nextProps.match)) {
|
||||||
|
this.resolve(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.disregardResolve = true; // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -193,7 +206,7 @@ class RouteContent extends Component {
|
||||||
const params = this.props.match.params;
|
const params = this.props.match.params;
|
||||||
const resolved = this.state.resolved;
|
const resolved = this.state.resolved;
|
||||||
|
|
||||||
if (!route.render && !route.component && route.link) {
|
if (!route.panelRender && !route.panelComponent && route.link) {
|
||||||
let link;
|
let link;
|
||||||
if (typeof route.link === 'function') {
|
if (typeof route.link === 'function') {
|
||||||
link = route.link(params);
|
link = route.link(params);
|
||||||
|
@ -211,11 +224,11 @@ class RouteContent extends Component {
|
||||||
resolved
|
resolved
|
||||||
};
|
};
|
||||||
|
|
||||||
let component;
|
let panel;
|
||||||
if (route.render) {
|
if (route.panelComponent) {
|
||||||
component = route.render(compProps);
|
panel = React.createElement(route.panelComponent, compProps);
|
||||||
} else if (route.component) {
|
} else if (route.panelRender) {
|
||||||
component = React.createElement(route.component, compProps, null);
|
panel = route.panelRender(compProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -226,7 +239,7 @@ class RouteContent extends Component {
|
||||||
<SecondaryNavBar className="visible-xs" route={route} params={params} resolved={resolved}/>
|
<SecondaryNavBar className="visible-xs" route={route} params={params} resolved={resolved}/>
|
||||||
</div>
|
</div>
|
||||||
{this.props.flashMessage}
|
{this.props.flashMessage}
|
||||||
{component}
|
{panel}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -291,14 +304,6 @@ class SectionContent extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getFlashMessageText() {
|
|
||||||
return this.state.flashMessageText;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFlashMessageSeverity() {
|
|
||||||
return this.state.flashMessageSeverity;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFlashMessage(severity, text) {
|
setFlashMessage(severity, text) {
|
||||||
this.setState({
|
this.setState({
|
||||||
flashMessageText: text,
|
flashMessageText: text,
|
||||||
|
@ -346,77 +351,6 @@ class SectionContent extends Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoutes(urlPrefix, resolve, parents, structure, navs) {
|
|
||||||
let routes = [];
|
|
||||||
for (let routeKey in structure) {
|
|
||||||
const entry = structure[routeKey];
|
|
||||||
|
|
||||||
let path = urlPrefix + routeKey;
|
|
||||||
let pathWithParams = path;
|
|
||||||
|
|
||||||
if (entry.extraParams) {
|
|
||||||
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
let entryResolve;
|
|
||||||
if (entry.resolve) {
|
|
||||||
entryResolve = Object.assign({}, resolve, entry.resolve);
|
|
||||||
} else {
|
|
||||||
entryResolve = resolve;
|
|
||||||
}
|
|
||||||
|
|
||||||
let navKeys;
|
|
||||||
const entryNavs = [];
|
|
||||||
if (entry.navs) {
|
|
||||||
navKeys = Object.keys(entry.navs);
|
|
||||||
|
|
||||||
for (const navKey of navKeys) {
|
|
||||||
const nav = entry.navs[navKey];
|
|
||||||
|
|
||||||
entryNavs.push({
|
|
||||||
title: nav.title,
|
|
||||||
visible: nav.visible,
|
|
||||||
link: nav.link,
|
|
||||||
externalLink: nav.externalLink
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = {
|
|
||||||
path: (pathWithParams === '' ? '/' : pathWithParams),
|
|
||||||
component: entry.component,
|
|
||||||
render: entry.render,
|
|
||||||
title: entry.title,
|
|
||||||
link: entry.link,
|
|
||||||
resolve: entryResolve,
|
|
||||||
parents,
|
|
||||||
navs: [...navs, ...entryNavs]
|
|
||||||
};
|
|
||||||
|
|
||||||
routes.push(route);
|
|
||||||
|
|
||||||
const childrenParents = [...parents, route];
|
|
||||||
|
|
||||||
if (entry.navs) {
|
|
||||||
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
|
|
||||||
const navKey = navKeys[navKeyIdx];
|
|
||||||
const nav = entry.navs[navKey];
|
|
||||||
|
|
||||||
const childNavs = [...entryNavs];
|
|
||||||
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
|
|
||||||
|
|
||||||
routes = routes.concat(this.getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.children) {
|
|
||||||
routes = routes.concat(this.getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return routes;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRoute(route) {
|
renderRoute(route) {
|
||||||
let flashMessage;
|
let flashMessage;
|
||||||
if (this.state.flashMessageText) {
|
if (this.state.flashMessageText) {
|
||||||
|
@ -429,7 +363,7 @@ class SectionContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let routes = this.getRoutes('', {}, [], this.props.structure, []);
|
let routes = getRoutes('', {}, [], this.props.structure, [], null, null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>{routes.map(x => this.renderRoute(x))}</Switch>
|
<Switch>{routes.map(x => this.renderRoute(x))}</Switch>
|
||||||
|
@ -526,44 +460,6 @@ class DropdownLink extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withPageHelpers(target) {
|
|
||||||
target = withErrorHandling(target);
|
|
||||||
|
|
||||||
const inst = target.prototype;
|
|
||||||
|
|
||||||
const contextTypes = target.contextTypes || {};
|
|
||||||
|
|
||||||
contextTypes.sectionContent = PropTypes.object.isRequired;
|
|
||||||
|
|
||||||
target.contextTypes = contextTypes;
|
|
||||||
|
|
||||||
inst.getFlashMessageText = function() {
|
|
||||||
return this.context.sectionContent.getFlashMessageText();
|
|
||||||
};
|
|
||||||
|
|
||||||
inst.getFlashMessageSeverity = function() {
|
|
||||||
return this.context.sectionContent.getFlashMessageSeverity();
|
|
||||||
};
|
|
||||||
|
|
||||||
inst.setFlashMessage = function(severity, text) {
|
|
||||||
return this.context.sectionContent.setFlashMessage(severity, text);
|
|
||||||
};
|
|
||||||
|
|
||||||
inst.navigateTo = function(path) {
|
|
||||||
return this.context.sectionContent.navigateTo(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
inst.navigateBack = function() {
|
|
||||||
return this.context.sectionContent.navigateBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
inst.navigateToWithFlashMessage = function(path, severity, text) {
|
|
||||||
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiresAuthenticatedUser(target) {
|
function requiresAuthenticatedUser(target) {
|
||||||
const comp1 = withPageHelpers(target);
|
const comp1 = withPageHelpers(target);
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,10 @@ class Table extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectInfo() {
|
updateSelectInfo() {
|
||||||
|
if (!this.jqSelectInfo) {
|
||||||
|
return; // If the table is updated very quickly after mounting, the datatable may not be initialized yet.
|
||||||
|
}
|
||||||
|
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
const count = this.selectionMap.size;
|
const count = this.selectionMap.size;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { DeleteModalDialog } from '../lib/modals';
|
||||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||||
import { UnsubscriptionMode } from '../../../shared/lists';
|
import { UnsubscriptionMode } from '../../../shared/lists';
|
||||||
import styles from "../lib/styles.scss";
|
import styles from "../lib/styles.scss";
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
|
@ -46,7 +47,7 @@ export default class CUD extends Component {
|
||||||
default_form: null,
|
default_form: null,
|
||||||
public_subscribe: true,
|
public_subscribe: true,
|
||||||
unsubscription_mode: UnsubscriptionMode.ONE_STEP,
|
unsubscription_mode: UnsubscriptionMode.ONE_STEP,
|
||||||
namespace: null
|
namespace: mailtrainConfig.user.namespace
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +103,7 @@ export default class CUD extends Component {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
|
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||||
|
|
||||||
const unsubcriptionModeOptions = [
|
const unsubcriptionModeOptions = [
|
||||||
{
|
{
|
||||||
|
@ -146,7 +148,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isEdit &&
|
{canDelete &&
|
||||||
<DeleteModalDialog
|
<DeleteModalDialog
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
|
@ -185,7 +187,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.entity.id}/delete`}/>}
|
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.entity.id}/delete`}/>}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,6 +16,8 @@ import validators from '../../../../shared/validators';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
|
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
|
||||||
import styles from "../../lib/styles.scss";
|
import styles from "../../lib/styles.scss";
|
||||||
|
import 'brace/mode/json';
|
||||||
|
import 'brace/mode/handlebars';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
|
@ -113,10 +115,7 @@ export default class CUD extends Component {
|
||||||
dateFormat: 'eur',
|
dateFormat: 'eur',
|
||||||
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
|
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
|
||||||
orderSubscribeBefore: 'end',
|
orderSubscribeBefore: 'end',
|
||||||
orderManageBefore: 'end',
|
orderManageBefore: 'end'
|
||||||
orderListOptions: [],
|
|
||||||
orderSubscribeOptions: [],
|
|
||||||
orderManageOptions: []
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,16 +281,6 @@ export default class CUD extends Component {
|
||||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof interoperableErrors.DependencyNotFoundError) {
|
|
||||||
this.setFormStatusMessage('danger',
|
|
||||||
<span>
|
|
||||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
|
||||||
{t('It seems that another field upon which sort field order was established has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -459,7 +448,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
{type !== 'option' &&
|
{type !== 'option' &&
|
||||||
<Fieldset label={t('Field order')}>
|
<Fieldset label={t('Field order')}>
|
||||||
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={getOrderOptions('order_list')} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
|
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={getOrderOptions('order_list')} help={t('Select the field before which this field should appear in listings. To exclude the field from listings, select "Not visible".')}/>
|
||||||
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={getOrderOptions('order_subscribe')} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
|
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={getOrderOptions('order_subscribe')} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
|
||||||
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={getOrderOptions('order_manage')} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
|
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={getOrderOptions('order_manage')} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default class List extends Component {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ data: 4, title: "#" },
|
{ data: 4, title: "#" },
|
||||||
{ data: 1, title: t('Name'),
|
{ data: 1, title: t('Name'),
|
||||||
render: (data, cmd, rowData) => rowData[2] === 'option' ? <span><span className="glyphicon glyphicon-record" aria-hidden="true"></span> {data}</span> : data
|
render: (data, cmd, rowData) => rowData[2] === 'option' ? <span><Icon icon="record"/> {data}</span> : data
|
||||||
},
|
},
|
||||||
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
|
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
|
||||||
{ data: 3, title: t('Merge Tag') },
|
{ data: 3, title: t('Merge Tag') },
|
||||||
|
|
|
@ -262,7 +262,7 @@ export default class CUD extends Component {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
selectedTemplate: 'layout',
|
selectedTemplate: 'layout',
|
||||||
namespace: null
|
namespace: mailtrainConfig.user.namespace
|
||||||
};
|
};
|
||||||
supplyDefaults(data);
|
supplyDefaults(data);
|
||||||
|
|
||||||
|
@ -341,6 +341,7 @@ export default class CUD extends Component {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
|
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||||
|
|
||||||
const templateOptGroups = [];
|
const templateOptGroups = [];
|
||||||
|
|
||||||
|
@ -369,7 +370,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isEdit &&
|
{canDelete &&
|
||||||
<DeleteModalDialog
|
<DeleteModalDialog
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
|
@ -433,7 +434,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/forms/${this.props.entity.id}/delete`}/>}
|
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/forms/${this.props.entity.id}/delete`}/>}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,8 +21,6 @@ import Share from '../shares/Share';
|
||||||
|
|
||||||
|
|
||||||
const getStructure = t => {
|
const getStructure = t => {
|
||||||
const subPaths = {};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'': {
|
'': {
|
||||||
title: t('Home'),
|
title: t('Home'),
|
||||||
|
@ -31,7 +29,7 @@ const getStructure = t => {
|
||||||
'lists': {
|
'lists': {
|
||||||
title: t('Lists'),
|
title: t('Lists'),
|
||||||
link: '/lists',
|
link: '/lists',
|
||||||
component: ListsList,
|
panelComponent: ListsList,
|
||||||
children: {
|
children: {
|
||||||
':listId([0-9]+)': {
|
':listId([0-9]+)': {
|
||||||
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
|
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
|
||||||
|
@ -47,7 +45,7 @@ const getStructure = t => {
|
||||||
},
|
},
|
||||||
link: params => `/lists/${params.listId}/subscriptions`,
|
link: params => `/lists/${params.listId}/subscriptions`,
|
||||||
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
|
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
|
||||||
render: props => <SubscriptionsList list={props.resolved.list} segments={props.resolved.segments} segmentId={qs.parse(props.location.search).segment} />,
|
panelRender: props => <SubscriptionsList list={props.resolved.list} segments={props.resolved.segments} segmentId={qs.parse(props.location.search).segment} />,
|
||||||
children: {
|
children: {
|
||||||
':subscriptionId([0-9]+)': {
|
':subscriptionId([0-9]+)': {
|
||||||
title: resolved => resolved.subscription.email,
|
title: resolved => resolved.subscription.email,
|
||||||
|
@ -60,7 +58,7 @@ const getStructure = t => {
|
||||||
':action(edit|delete)': {
|
':action(edit|delete)': {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
|
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
|
||||||
render: props => <SubscriptionsCUD action={props.match.params.action} entity={props.resolved.subscription} list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
|
panelRender: props => <SubscriptionsCUD action={props.match.params.action} entity={props.resolved.subscription} list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -69,20 +67,20 @@ const getStructure = t => {
|
||||||
resolve: {
|
resolve: {
|
||||||
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
|
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
|
||||||
},
|
},
|
||||||
render: props => <SubscriptionsCUD action="create" list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
|
panelRender: props => <SubscriptionsCUD action="create" list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
|
||||||
}
|
}
|
||||||
} },
|
} },
|
||||||
':action(edit|delete)': {
|
':action(edit|delete)': {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/lists/${params.listId}/edit`,
|
link: params => `/lists/${params.listId}/edit`,
|
||||||
visible: resolved => resolved.list.permissions.includes('edit'),
|
visible: resolved => resolved.list.permissions.includes('edit'),
|
||||||
render: props => <ListsCUD action={props.match.params.action} entity={props.resolved.list} />
|
panelRender: props => <ListsCUD action={props.match.params.action} entity={props.resolved.list} />
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
title: t('Fields'),
|
title: t('Fields'),
|
||||||
link: params => `/lists/${params.listId}/fields/`,
|
link: params => `/lists/${params.listId}/fields/`,
|
||||||
visible: resolved => resolved.list.permissions.includes('manageFields'),
|
visible: resolved => resolved.list.permissions.includes('manageFields'),
|
||||||
render: props => <FieldsList list={props.resolved.list} />,
|
panelRender: props => <FieldsList list={props.resolved.list} />,
|
||||||
children: {
|
children: {
|
||||||
':fieldId([0-9]+)': {
|
':fieldId([0-9]+)': {
|
||||||
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
|
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
|
||||||
|
@ -95,7 +93,7 @@ const getStructure = t => {
|
||||||
':action(edit|delete)': {
|
':action(edit|delete)': {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
|
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
|
||||||
render: props => <FieldsCUD action={props.match.params.action} entity={props.resolved.field} list={props.resolved.list} fields={props.resolved.fields} />
|
panelRender: props => <FieldsCUD action={props.match.params.action} entity={props.resolved.field} list={props.resolved.list} fields={props.resolved.fields} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -104,7 +102,7 @@ const getStructure = t => {
|
||||||
resolve: {
|
resolve: {
|
||||||
fields: params => `/rest/fields/${params.listId}`
|
fields: params => `/rest/fields/${params.listId}`
|
||||||
},
|
},
|
||||||
render: props => <FieldsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
|
panelRender: props => <FieldsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -112,7 +110,7 @@ const getStructure = t => {
|
||||||
title: t('Segments'),
|
title: t('Segments'),
|
||||||
link: params => `/lists/${params.listId}/segments`,
|
link: params => `/lists/${params.listId}/segments`,
|
||||||
visible: resolved => resolved.list.permissions.includes('manageSegments'),
|
visible: resolved => resolved.list.permissions.includes('manageSegments'),
|
||||||
render: props => <SegmentsList list={props.resolved.list} />,
|
panelRender: props => <SegmentsList list={props.resolved.list} />,
|
||||||
children: {
|
children: {
|
||||||
':segmentId([0-9]+)': {
|
':segmentId([0-9]+)': {
|
||||||
title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
|
title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
|
||||||
|
@ -125,7 +123,7 @@ const getStructure = t => {
|
||||||
':action(edit|delete)': {
|
':action(edit|delete)': {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
|
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
|
||||||
render: props => <SegmentsCUD action={props.match.params.action} entity={props.resolved.segment} list={props.resolved.list} fields={props.resolved.fields} />
|
panelRender: props => <SegmentsCUD action={props.match.params.action} entity={props.resolved.segment} list={props.resolved.list} fields={props.resolved.fields} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -134,7 +132,7 @@ const getStructure = t => {
|
||||||
resolve: {
|
resolve: {
|
||||||
fields: params => `/rest/fields/${params.listId}`
|
fields: params => `/rest/fields/${params.listId}`
|
||||||
},
|
},
|
||||||
render: props => <SegmentsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
|
panelRender: props => <SegmentsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -142,18 +140,18 @@ const getStructure = t => {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/lists/${params.listId}/share`,
|
link: params => `/lists/${params.listId}/share`,
|
||||||
visible: resolved => resolved.list.permissions.includes('share'),
|
visible: resolved => resolved.list.permissions.includes('share'),
|
||||||
render: props => <Share title={t('Share')} entity={props.resolved.list} entityTypeId="list" />
|
panelRender: props => <Share title={t('Share')} entity={props.resolved.list} entityTypeId="list" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
render: props => <ListsCUD action="create" />
|
panelRender: props => <ListsCUD action="create" />
|
||||||
},
|
},
|
||||||
forms: {
|
forms: {
|
||||||
title: t('Custom Forms'),
|
title: t('Custom Forms'),
|
||||||
link: '/lists/forms',
|
link: '/lists/forms',
|
||||||
component: FormsList,
|
panelComponent: FormsList,
|
||||||
children: {
|
children: {
|
||||||
':formsId([0-9]+)': {
|
':formsId([0-9]+)': {
|
||||||
title: resolved => t('Custom Forms "{{name}}"', {name: resolved.forms.name}),
|
title: resolved => t('Custom Forms "{{name}}"', {name: resolved.forms.name}),
|
||||||
|
@ -166,19 +164,19 @@ const getStructure = t => {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/lists/forms/${params.formsId}/edit`,
|
link: params => `/lists/forms/${params.formsId}/edit`,
|
||||||
visible: resolved => resolved.forms.permissions.includes('edit'),
|
visible: resolved => resolved.forms.permissions.includes('edit'),
|
||||||
render: props => <FormsCUD action={props.match.params.action} entity={props.resolved.forms} />
|
panelRender: props => <FormsCUD action={props.match.params.action} entity={props.resolved.forms} />
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/lists/forms/${params.formsId}/share`,
|
link: params => `/lists/forms/${params.formsId}/share`,
|
||||||
visible: resolved => resolved.forms.permissions.includes('share'),
|
visible: resolved => resolved.forms.permissions.includes('share'),
|
||||||
render: props => <Share title={t('Share')} entity={props.resolved.forms} entityTypeId="customForm" />
|
panelRender: props => <Share title={t('Share')} entity={props.resolved.forms} entityTypeId="customForm" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
render: props => <FormsCUD action="create" />
|
panelRender: props => <FormsCUD action="create" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,6 +192,6 @@ export default function() {
|
||||||
<I18nextProvider i18n={ i18n }><Section root='/lists' structure={getStructure}/></I18nextProvider>,
|
<I18nextProvider i18n={ i18n }><Section root='/lists' structure={getStructure}/></I18nextProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -178,16 +178,6 @@ export default class CUD extends Component {
|
||||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof interoperableErrors.DependencyNotFoundError) {
|
|
||||||
this.setFormStatusMessage('danger',
|
|
||||||
<span>
|
|
||||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
|
||||||
{t('It seems that another field upon which sort field order was established has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,19 +64,19 @@ export default class List extends Component {
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async deleteSubscription(id) {
|
async deleteSubscription(id) {
|
||||||
await axios.delete(`/rest/subscriptions/${this.props.list.id}/${id}`);
|
await axios.delete(`/rest/subscriptions/${this.props.list.id}/${id}`);
|
||||||
this.subscriptionsTable.refresh();
|
this.blacklistTable.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async unsubscribeSubscription(id) {
|
async unsubscribeSubscription(id) {
|
||||||
await axios.post(`/rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`);
|
await axios.post(`/rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`);
|
||||||
this.subscriptionsTable.refresh();
|
this.blacklistTable.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async blacklistSubscription(id) {
|
async blacklistSubscription(email) {
|
||||||
await axios.post(`/rest/XXX/${this.props.list.id}/${id}`); // FIXME - add url one the blacklist functionality is in
|
await axios.post("/rest/blacklist", { email });
|
||||||
this.subscriptionsTable.refresh();
|
this.blacklistTable.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -86,11 +86,11 @@ export default class List extends Component {
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ data: 2, title: t('Email') },
|
{ data: 2, title: t('Email') },
|
||||||
{ data: 3, title: t('Status'), render: data => this.subscriptionStatusLabels[data] },
|
{ data: 3, title: t('Status'), render: (data, display, rowData) => this.subscriptionStatusLabels[data] + (rowData[5] ? ', ' + t('Blacklisted') : '') },
|
||||||
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
|
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
|
||||||
];
|
];
|
||||||
|
|
||||||
let colIdx = 5;
|
let colIdx = 6;
|
||||||
|
|
||||||
for (const fld of list.listFields) {
|
for (const fld of list.listFields) {
|
||||||
|
|
||||||
|
@ -123,11 +123,12 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME - add condition here to show it only if not blacklisted already
|
if (!data[5]) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="ban-circle" title={t('Blacklist')}/>,
|
label: <Icon icon="ban-circle" title={t('Blacklist')}/>,
|
||||||
action: () => this.blacklistSubscription(data[0])
|
action: () => this.blacklistSubscription(data[2])
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="remove" title={t('Remove')}/>,
|
label: <Icon icon="remove" title={t('Remove')}/>,
|
||||||
|
@ -169,7 +170,7 @@ export default class List extends Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Table ref={node => this.subscriptionsTable = node} withHeader dataUrl={dataUrl} columns={columns} />
|
<Table ref={node => this.blacklistTable = node} withHeader dataUrl={dataUrl} columns={columns} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {SubscriptionStatus} from "../../../../shared/lists";
|
||||||
import {ACEEditor, CheckBoxGroup, DatePicker, Dropdown, InputField, RadioGroup, TextArea} from "../../lib/form";
|
import {ACEEditor, CheckBoxGroup, DatePicker, Dropdown, InputField, RadioGroup, TextArea} from "../../lib/form";
|
||||||
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
||||||
import {getFieldKey} from '../../../../shared/lists';
|
import {getFieldKey} from '../../../../shared/lists';
|
||||||
|
import 'brace/mode/json';
|
||||||
|
|
||||||
export function getSubscriptionStatusLabels(t) {
|
export function getSubscriptionStatusLabels(t) {
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import axios from '../lib/axios';
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||||
import {DeleteModalDialog} from "../lib/modals";
|
import {DeleteModalDialog} from "../lib/modals";
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
|
@ -34,10 +35,6 @@ export default class CUD extends Component {
|
||||||
return this.props.entity && this.props.entity.id === 1; /* Global namespace id */
|
return this.props.entity && this.props.entity.id === 1; /* Global namespace id */
|
||||||
}
|
}
|
||||||
|
|
||||||
isDelete() {
|
|
||||||
return this.props.match.params.action === 'delete';
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNsIdSubtree(data) {
|
removeNsIdSubtree(data) {
|
||||||
for (let idx = 0; idx < data.length; idx++) {
|
for (let idx = 0; idx < data.length; idx++) {
|
||||||
const entry = data[idx];
|
const entry = data[idx];
|
||||||
|
@ -84,7 +81,7 @@ export default class CUD extends Component {
|
||||||
this.populateFormValues({
|
this.populateFormValues({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
namespace: null
|
namespace: mailtrainConfig.user.namespace
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,10 +176,11 @@ export default class CUD extends Component {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
|
const canDelete = isEdit && !this.isEditGlobal() && !this.hasChildren && this.props.entity.permissions.includes('delete');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!this.isEditGlobal() && !this.hasChildren && isEdit &&
|
{canDelete &&
|
||||||
<DeleteModalDialog
|
<DeleteModalDialog
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
|
@ -205,7 +203,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
{!this.isEditGlobal() && !this.hasChildren && isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/namespaces/${this.props.entity.id}/delete`}/>}
|
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/namespaces/${this.props.entity.id}/delete`}/>}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@ const getStructure = t => ({
|
||||||
namespaces: {
|
namespaces: {
|
||||||
title: t('Namespaces'),
|
title: t('Namespaces'),
|
||||||
link: '/namespaces',
|
link: '/namespaces',
|
||||||
component: List,
|
panelComponent: List,
|
||||||
children: {
|
children: {
|
||||||
':namespaceId([0-9]+)': {
|
':namespaceId([0-9]+)': {
|
||||||
title: resolved => t('Namespace "{{name}}"', {name: resolved.namespace.name}),
|
title: resolved => t('Namespace "{{name}}"', {name: resolved.namespace.name}),
|
||||||
|
@ -31,19 +31,19 @@ const getStructure = t => ({
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/namespaces/${params.namespaceId}/edit`,
|
link: params => `/namespaces/${params.namespaceId}/edit`,
|
||||||
visible: resolved => resolved.namespace.permissions.includes('edit'),
|
visible: resolved => resolved.namespace.permissions.includes('edit'),
|
||||||
render: props => <CUD action={props.match.params.action} entity={props.resolved.namespace} />
|
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.namespace} />
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/namespaces/${params.namespaceId}/share`,
|
link: params => `/namespaces/${params.namespaceId}/share`,
|
||||||
visible: resolved => resolved.namespace.permissions.includes('share'),
|
visible: resolved => resolved.namespace.permissions.includes('share'),
|
||||||
render: props => <Share title={t('Share')} entity={props.resolved.namespace} entityTypeId="namespace" />
|
panelRender: props => <Share title={t('Share')} entity={props.resolved.namespace} entityTypeId="namespace" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
render: props => <CUD action="create" />
|
panelRender: props => <CUD action="create" />
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,6 @@ export default function() {
|
||||||
<I18nextProvider i18n={ i18n }><Section root='/namespaces' structure={getStructure}/></I18nextProvider>,
|
<I18nextProvider i18n={ i18n }><Section root='/namespaces' structure={getStructure}/></I18nextProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||||
import {DeleteModalDialog} from "../lib/modals";
|
import {DeleteModalDialog} from "../lib/modals";
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
|
@ -65,7 +66,7 @@ export default class CUD extends Component {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
report_template: null,
|
report_template: null,
|
||||||
namespace: null,
|
namespace: mailtrainConfig.user.namespace,
|
||||||
user_fields: null
|
user_fields: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -100,7 +101,7 @@ export default class CUD extends Component {
|
||||||
const selection = state.getIn([fldId, 'value']) || [];
|
const selection = state.getIn([fldId, 'value']) || [];
|
||||||
|
|
||||||
if (spec.maxOccurences === 1) {
|
if (spec.maxOccurences === 1) {
|
||||||
if (spec.minOccurences === 1 && (selection === null || selection === undefined)) {
|
if (spec.minOccurences === 1 && (selection === null || selection === undefined)) { // FIXME - this does not seem to correspond with selectionAsArray
|
||||||
state.setIn([fldId, 'error'], t('Exactly one item has to be selected'));
|
state.setIn([fldId, 'error'], t('Exactly one item has to be selected'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -119,7 +120,7 @@ export default class CUD extends Component {
|
||||||
async submitHandler() {
|
async submitHandler() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
if (!this.getFormValue('user_fields')) {
|
if (this.getFormValue('report_template') && !this.getFormValue('user_fields')) {
|
||||||
this.setFormStatusMessage('warning', t('Report parameters are not selected. Wait for them to get displayed and then fill them in.'));
|
this.setFormStatusMessage('warning', t('Report parameters are not selected. Wait for them to get displayed and then fill them in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -160,9 +161,9 @@ export default class CUD extends Component {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
|
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||||
|
|
||||||
const reportTemplateColumns = [
|
const reportTemplateColumns = [
|
||||||
{ data: 0, title: "#" },
|
|
||||||
{ data: 1, title: t('Name') },
|
{ data: 1, title: t('Name') },
|
||||||
{ data: 2, title: t('Description') },
|
{ data: 2, title: t('Description') },
|
||||||
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() }
|
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() }
|
||||||
|
@ -213,7 +214,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isEdit &&
|
{canDelete &&
|
||||||
<DeleteModalDialog
|
<DeleteModalDialog
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
|
@ -246,7 +247,9 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/${this.props.entity.id}/delete`}/>}
|
{canDelete &&
|
||||||
|
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/${this.props.entity.id}/delete`}/>
|
||||||
|
}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,7 +28,7 @@ const getStructure = t => {
|
||||||
'reports': {
|
'reports': {
|
||||||
title: t('Reports'),
|
title: t('Reports'),
|
||||||
link: '/reports',
|
link: '/reports',
|
||||||
component: ReportsList,
|
panelComponent: ReportsList,
|
||||||
children: {
|
children: {
|
||||||
':reportId([0-9]+)': {
|
':reportId([0-9]+)': {
|
||||||
title: resolved => t('Report "{{name}}"', {name: resolved.report.name}),
|
title: resolved => t('Report "{{name}}"', {name: resolved.report.name}),
|
||||||
|
@ -41,13 +41,13 @@ const getStructure = t => {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/reports/${params.reportId}/edit`,
|
link: params => `/reports/${params.reportId}/edit`,
|
||||||
visible: resolved => resolved.report.permissions.includes('edit'),
|
visible: resolved => resolved.report.permissions.includes('edit'),
|
||||||
render: props => <ReportsCUD action={props.match.params.action} entity={props.resolved.report} />
|
panelRender: props => <ReportsCUD action={props.match.params.action} entity={props.resolved.report} />
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
title: t('View'),
|
title: t('View'),
|
||||||
link: params => `/reports/${params.reportId}/view`,
|
link: params => `/reports/${params.reportId}/view`,
|
||||||
visible: resolved => resolved.report.permissions.includes('viewContent') && resolved.report.state === ReportState.FINISHED && resolved.report.mime_type === 'text/html',
|
visible: resolved => resolved.report.permissions.includes('viewContent') && resolved.report.state === ReportState.FINISHED && resolved.report.mime_type === 'text/html',
|
||||||
render: props => (<ReportsView {...props} />),
|
panelRender: props => (<ReportsView {...props} />),
|
||||||
},
|
},
|
||||||
download: {
|
download: {
|
||||||
title: t('Download'),
|
title: t('Download'),
|
||||||
|
@ -58,24 +58,24 @@ const getStructure = t => {
|
||||||
title: t('Output'),
|
title: t('Output'),
|
||||||
link: params => `/reports/${params.reportId}/output`,
|
link: params => `/reports/${params.reportId}/output`,
|
||||||
visible: resolved => resolved.report.permissions.includes('viewOutput'),
|
visible: resolved => resolved.report.permissions.includes('viewOutput'),
|
||||||
render: props => (<ReportsOutput {...props} />)
|
panelRender: props => (<ReportsOutput {...props} />)
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/reports/${params.reportId}/share`,
|
link: params => `/reports/${params.reportId}/share`,
|
||||||
visible: resolved => resolved.report.permissions.includes('share'),
|
visible: resolved => resolved.report.permissions.includes('share'),
|
||||||
render: props => <Share title={t('Share')} entity={props.resolved.report} entityTypeId="report" />
|
panelRender: props => <Share title={t('Share')} entity={props.resolved.report} entityTypeId="report" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
render: props => <ReportsCUD action="create" />
|
panelRender: props => <ReportsCUD action="create" />
|
||||||
},
|
},
|
||||||
'templates': {
|
'templates': {
|
||||||
title: t('Templates'),
|
title: t('Templates'),
|
||||||
link: '/reports/templates',
|
link: '/reports/templates',
|
||||||
component: ReportTemplatesList,
|
panelComponent: ReportTemplatesList,
|
||||||
children: {
|
children: {
|
||||||
':templateId([0-9]+)': {
|
':templateId([0-9]+)': {
|
||||||
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
|
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
|
||||||
|
@ -88,20 +88,20 @@ const getStructure = t => {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/reports/templates/${params.templateId}/edit`,
|
link: params => `/reports/templates/${params.templateId}/edit`,
|
||||||
visible: resolved => mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && resolved.template.permissions.includes('edit'),
|
visible: resolved => mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && resolved.template.permissions.includes('edit'),
|
||||||
render: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
panelRender: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/reports/templates/${params.templateId}/share`,
|
link: params => `/reports/templates/${params.templateId}/share`,
|
||||||
visible: resolved => resolved.template.permissions.includes('share'),
|
visible: resolved => resolved.template.permissions.includes('share'),
|
||||||
render: props => <Share title={t('Share')} entity={props.resolved.template} entityTypeId="reportTemplate" />
|
panelRender: props => <Share title={t('Share')} entity={props.resolved.template} entityTypeId="reportTemplate" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
extraParams: [':wizard?'],
|
extraParams: [':wizard?'],
|
||||||
render: props => <ReportTemplatesCUD action="create" wizard={props.match.params.wizard} />
|
panelRender: props => <ReportTemplatesCUD action="create" wizard={props.match.params.wizard} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,10 @@ import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEdit
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||||
import { validateNamespace, NamespaceSelect } from '../../lib/namespace';
|
import { validateNamespace, NamespaceSelect } from '../../lib/namespace';
|
||||||
import {DeleteModalDialog} from "../../lib/modals";
|
import {DeleteModalDialog} from "../../lib/modals";
|
||||||
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
import 'brace/mode/javascript';
|
||||||
|
import 'brace/mode/json';
|
||||||
|
import 'brace/mode/handlebars';
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
|
@ -45,7 +49,7 @@ export default class CUD extends Component {
|
||||||
this.populateFormValues({
|
this.populateFormValues({
|
||||||
name: '',
|
name: '',
|
||||||
description: 'Generates a campaign report listing all subscribers along with their statistics.',
|
description: 'Generates a campaign report listing all subscribers along with their statistics.',
|
||||||
namespace: null,
|
namespace: mailtrainConfig.user.namespace,
|
||||||
mime_type: 'text/html',
|
mime_type: 'text/html',
|
||||||
user_fields:
|
user_fields:
|
||||||
'[\n' +
|
'[\n' +
|
||||||
|
@ -95,7 +99,7 @@ export default class CUD extends Component {
|
||||||
this.populateFormValues({
|
this.populateFormValues({
|
||||||
name: '',
|
name: '',
|
||||||
description: 'Generates a campaign report with results are aggregated by some "Country" custom field.',
|
description: 'Generates a campaign report with results are aggregated by some "Country" custom field.',
|
||||||
namespace: null,
|
namespace: mailtrainConfig.user.namespace,
|
||||||
mime_type: 'text/html',
|
mime_type: 'text/html',
|
||||||
user_fields:
|
user_fields:
|
||||||
'[\n' +
|
'[\n' +
|
||||||
|
@ -166,7 +170,7 @@ export default class CUD extends Component {
|
||||||
this.populateFormValues({
|
this.populateFormValues({
|
||||||
name: '',
|
name: '',
|
||||||
description: 'Exports a list as a CSV file.',
|
description: 'Exports a list as a CSV file.',
|
||||||
namespace: null,
|
namespace: mailtrainConfig.user.namespace,
|
||||||
mime_type: 'text/csv',
|
mime_type: 'text/csv',
|
||||||
user_fields:
|
user_fields:
|
||||||
'[\n' +
|
'[\n' +
|
||||||
|
@ -191,7 +195,7 @@ export default class CUD extends Component {
|
||||||
this.populateFormValues({
|
this.populateFormValues({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
namespace: null,
|
namespace: mailtrainConfig.user.namespace,
|
||||||
mime_type: 'text/html',
|
mime_type: 'text/html',
|
||||||
user_fields: '',
|
user_fields: '',
|
||||||
js: '',
|
js: '',
|
||||||
|
@ -270,10 +274,11 @@ export default class CUD extends Component {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
|
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isEdit &&
|
{canDelete &&
|
||||||
<DeleteModalDialog
|
<DeleteModalDialog
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
|
@ -299,7 +304,9 @@ export default class CUD extends Component {
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
|
||||||
|
{canDelete &&
|
||||||
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/templates/${this.props.entity.id}/delete`}/>
|
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/templates/${this.props.entity.id}/delete`}/>
|
||||||
|
}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
:
|
:
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default class CUD extends Component {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
password2: '',
|
password2: '',
|
||||||
namespace: null
|
namespace: mailtrainConfig.user.namespace
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ export default class CUD extends Component {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
const userId = this.getFormValue('id');
|
const userId = this.getFormValue('id');
|
||||||
const canDelete = userId !== 1 && mailtrainConfig.userId !== userId;
|
const canDelete = isEdit && userId !== 1 && mailtrainConfig.user.id !== userId;
|
||||||
|
|
||||||
const rolesColumns = [
|
const rolesColumns = [
|
||||||
{ data: 1, title: "Name" },
|
{ data: 1, title: "Name" },
|
||||||
|
@ -192,7 +192,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isEdit && canDelete &&
|
{canDelete &&
|
||||||
<DeleteModalDialog
|
<DeleteModalDialog
|
||||||
stateOwner={this}
|
stateOwner={this}
|
||||||
visible={this.props.action === 'delete'}
|
visible={this.props.action === 'delete'}
|
||||||
|
@ -220,7 +220,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
{isEdit && canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete User')} linkTo={`/users/${this.props.entity.id}/delete`}/>}
|
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete User')} linkTo={`/users/${this.props.entity.id}/delete`}/>}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, {Component} from "react";
|
||||||
import { translate } from 'react-i18next';
|
import {translate} from "react-i18next";
|
||||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
|
import {NavButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from "../lib/page";
|
||||||
import { Table } from '../lib/table';
|
import {Table} from "../lib/table";
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from "mailtrainConfig";
|
||||||
import {Icon} from "../lib/bootstrap-components";
|
import {Icon} from "../lib/bootstrap-components";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
|
|
@ -19,7 +19,7 @@ const getStructure = t => {
|
||||||
users: {
|
users: {
|
||||||
title: t('Users'),
|
title: t('Users'),
|
||||||
link: '/users',
|
link: '/users',
|
||||||
component: List,
|
panelComponent: List,
|
||||||
children: {
|
children: {
|
||||||
':userId([0-9]+)': {
|
':userId([0-9]+)': {
|
||||||
title: resolved => t('User "{{name}}"', {name: resolved.user.name}),
|
title: resolved => t('User "{{name}}"', {name: resolved.user.name}),
|
||||||
|
@ -31,18 +31,18 @@ const getStructure = t => {
|
||||||
':action(edit|delete)': {
|
':action(edit|delete)': {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/users/${params.userId}/edit`,
|
link: params => `/users/${params.userId}/edit`,
|
||||||
render: props => <CUD action={props.match.params.action} entity={props.resolved.user} />
|
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.user} />
|
||||||
},
|
},
|
||||||
shares: {
|
shares: {
|
||||||
title: t('Shares'),
|
title: t('Shares'),
|
||||||
link: params => `/users/${params.userId}/shares`,
|
link: params => `/users/${params.userId}/shares`,
|
||||||
render: props => <UserShares user={props.resolved.user} />
|
panelRender: props => <UserShares user={props.resolved.user} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
render: props => <CUD action="create" />
|
panelRender: props => <CUD action="create" />
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,6 @@ export default function() {
|
||||||
<I18nextProvider i18n={ i18n }><Section root='/users' structure={getStructure}/></I18nextProvider>,
|
<I18nextProvider i18n={ i18n }><Section root='/users' structure={getStructure}/></I18nextProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ module.exports = {
|
||||||
users: ['babel-polyfill', './src/users/root.js'],
|
users: ['babel-polyfill', './src/users/root.js'],
|
||||||
account: ['babel-polyfill', './src/account/root.js'],
|
account: ['babel-polyfill', './src/account/root.js'],
|
||||||
reports: ['babel-polyfill', './src/reports/root.js'],
|
reports: ['babel-polyfill', './src/reports/root.js'],
|
||||||
lists: ['babel-polyfill', './src/lists/root.js']
|
lists: ['babel-polyfill', './src/lists/root.js'],
|
||||||
|
blacklist: ['babel-polyfill', './src/blacklist/root.js']
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
library: 'MailtrainReactBody',
|
library: 'MailtrainReactBody',
|
||||||
|
|
|
@ -196,7 +196,7 @@ browser="phantomjs"
|
||||||
name="Master"
|
name="Master"
|
||||||
admin=true
|
admin=true
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["rebuildPermissions", "createJavascriptWithROAccess"]
|
permissions=["rebuildPermissions", "createJavascriptWithROAccess", "manageBlacklist"]
|
||||||
rootNamespaceRole="master"
|
rootNamespaceRole="master"
|
||||||
|
|
||||||
[roles.namespace.master]
|
[roles.namespace.master]
|
||||||
|
|
|
@ -19,7 +19,10 @@ async function getAnonymousConfig(context) {
|
||||||
async function getAuthenticatedConfig(context) {
|
async function getAuthenticatedConfig(context) {
|
||||||
return {
|
return {
|
||||||
defaultCustomFormValues: await forms.getDefaultCustomFormValues(),
|
defaultCustomFormValues: await forms.getDefaultCustomFormValues(),
|
||||||
userId: context.user.id,
|
user: {
|
||||||
|
id: context.user.id,
|
||||||
|
namespace: context.user.namespace
|
||||||
|
},
|
||||||
globalPermissions: shares.getGlobalPermissions(context)
|
globalPermissions: shares.getGlobalPermissions(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ async function ajaxListTx(tx, params, queryFun, columns, options) {
|
||||||
const query = queryFun(tx);
|
const query = queryFun(tx);
|
||||||
query.whereIn(columnsNames[parseInt(params.column)], params.values);
|
query.whereIn(columnsNames[parseInt(params.column)], params.values);
|
||||||
query.select(columnsSelect);
|
query.select(columnsSelect);
|
||||||
|
query.options({rowsAsArray:true});
|
||||||
|
|
||||||
const rows = await query;
|
const rows = await query;
|
||||||
const rowsOfArray = rows.map(row => Object.keys(row).map(key => row[key]));
|
const rowsOfArray = rows.map(row => Object.keys(row).map(key => row[key]));
|
||||||
|
|
|
@ -7,7 +7,7 @@ const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
async function validateEntity(tx, entity) {
|
async function validateEntity(tx, entity) {
|
||||||
enforce(entity.namespace, 'Entity namespace not set');
|
enforce(entity.namespace, 'Entity namespace not set');
|
||||||
if (!await tx('namespaces').where('id', entity.namespace).first()) {
|
if (!await tx('namespaces').where('id', entity.namespace).first()) {
|
||||||
throw new interoperableErrors.DependencyNotFoundError();
|
throw new interoperableErrors.NamespaceNotFoundError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
61
models/blacklist.js
Normal file
61
models/blacklist.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const knex = require('../lib/knex');
|
||||||
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
|
const shares = require('./shares');
|
||||||
|
const tools = require('../lib/tools-async');
|
||||||
|
|
||||||
|
async function listDTAjax(context, params) {
|
||||||
|
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||||
|
|
||||||
|
return await dtHelpers.ajaxList(
|
||||||
|
params,
|
||||||
|
builder => builder
|
||||||
|
.from('blacklist'),
|
||||||
|
['blacklist.email']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add(context, email) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||||
|
|
||||||
|
const existing = await tx('blacklist').where('email', email).first();
|
||||||
|
if (!existing) {
|
||||||
|
await tx('blacklist').insert({email});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(context, email) {
|
||||||
|
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||||
|
await knex('blacklist').where('email', email).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isBlacklisted(email) {
|
||||||
|
const existing = await knex('blacklist').where('email', email).first();
|
||||||
|
return !!existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serverValidate(context, data) {
|
||||||
|
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
if (data.email) {
|
||||||
|
const user = await knex('blacklist').where('email', data.email).first();
|
||||||
|
|
||||||
|
result.email = {};
|
||||||
|
result.email.invalid = await tools.validateEmail(data.email) !== 0;
|
||||||
|
result.email.exists = !!user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listDTAjax,
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
isBlacklisted,
|
||||||
|
serverValidate
|
||||||
|
};
|
|
@ -208,6 +208,8 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||||
|
|
||||||
|
const listTable = getTableName(listId);
|
||||||
|
|
||||||
// All the data transformation below is to reuse ajaxListTx and groupSubscription methods so as to keep the code DRY
|
// All the data transformation below is to reuse ajaxListTx and groupSubscription methods so as to keep the code DRY
|
||||||
// We first construct the columns to contain all which is supposed to be show and extraColumns which contain
|
// We first construct the columns to contain all which is supposed to be show and extraColumns which contain
|
||||||
// everything else that constitutes the subscription.
|
// everything else that constitutes the subscription.
|
||||||
|
@ -218,7 +220,14 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||||
const listFlds = await fields.listByOrderListTx(tx, listId, ['column', 'id']);
|
const listFlds = await fields.listByOrderListTx(tx, listId, ['column', 'id']);
|
||||||
|
|
||||||
const columns = ['id', 'cid', 'email', 'status', 'created'];
|
const columns = [
|
||||||
|
listTable + '.id',
|
||||||
|
listTable + '.cid',
|
||||||
|
listTable + '.email',
|
||||||
|
listTable + '.status',
|
||||||
|
listTable + '.created',
|
||||||
|
{ name: 'blacklisted', raw: 'not isnull(blacklist.email)' }
|
||||||
|
];
|
||||||
const extraColumns = [];
|
const extraColumns = [];
|
||||||
let listFldIdx = columns.length;
|
let listFldIdx = columns.length;
|
||||||
const idxMap = {};
|
const idxMap = {};
|
||||||
|
@ -228,10 +237,10 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
const fld = groupedFieldsMap[fldKey];
|
const fld = groupedFieldsMap[fldKey];
|
||||||
|
|
||||||
if (fld.column) {
|
if (fld.column) {
|
||||||
columns.push(fld.column);
|
columns.push(listTable + '.' + fld.column);
|
||||||
} else {
|
} else {
|
||||||
columns.push({
|
columns.push({
|
||||||
name: fldKey,
|
name: listTable + '.' + fldKey,
|
||||||
raw: 0
|
raw: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -245,14 +254,14 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
|
|
||||||
if (fld.column) {
|
if (fld.column) {
|
||||||
if (!(fldKey in idxMap)) {
|
if (!(fldKey in idxMap)) {
|
||||||
extraColumns.push(fld.column);
|
extraColumns.push(listTable + '.' + fld.column);
|
||||||
idxMap[fldKey] = listFldIdx;
|
idxMap[fldKey] = listFldIdx;
|
||||||
listFldIdx += 1;
|
listFldIdx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
for (const optionColumn in fld.groupedOptions) {
|
for (const optionColumn in fld.groupedOptions) {
|
||||||
extraColumns.push(optionColumn);
|
extraColumns.push(listTable + '.' + optionColumn);
|
||||||
idxMap[optionColumn] = listFldIdx;
|
idxMap[optionColumn] = listFldIdx;
|
||||||
listFldIdx += 1;
|
listFldIdx += 1;
|
||||||
}
|
}
|
||||||
|
@ -265,7 +274,10 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
tx,
|
tx,
|
||||||
params,
|
params,
|
||||||
builder => {
|
builder => {
|
||||||
const query = builder.from(getTableName(listId));
|
const query = builder
|
||||||
|
.from(listTable)
|
||||||
|
.leftOuterJoin('blacklist', listTable + '.email', 'blacklist.email')
|
||||||
|
;
|
||||||
query.where(function() {
|
query.where(function() {
|
||||||
addSegmentQuery(this);
|
addSegmentQuery(this);
|
||||||
});
|
});
|
||||||
|
|
|
@ -267,6 +267,8 @@ async function getByUsernameIfPasswordMatch(username, password) {
|
||||||
throw new interoperableErrors.IncorrectPasswordError();
|
throw new interoperableErrors.IncorrectPasswordError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete user.password;
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
10
routes/blacklist-legacy-integration.js
Normal file
10
routes/blacklist-legacy-integration.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('../lib/translate')._;
|
||||||
|
const clientHelpers = require('../lib/client-helpers');
|
||||||
|
|
||||||
|
const router = require('../lib/router-async').create();
|
||||||
|
|
||||||
|
clientHelpers.registerRootRoute(router, 'blacklist', _('Blacklist'));
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -1,68 +0,0 @@
|
||||||
'use strict';
|
|
||||||
let express = require('express');
|
|
||||||
let router = new express.Router();
|
|
||||||
let passport = require('../lib/passport');
|
|
||||||
let htmlescape = require('escape-html');
|
|
||||||
let blacklist = require('../lib/models/blacklist');
|
|
||||||
let tools = require('../lib/tools');
|
|
||||||
let helpers = require('../lib/helpers');
|
|
||||||
let _ = require('../lib/translate')._;
|
|
||||||
|
|
||||||
router.all('/*', (req, res, next) => {
|
|
||||||
if (!req.user) {
|
|
||||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
|
||||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
|
||||||
}
|
|
||||||
res.setSelectedMenu('blacklist');
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/', passport.csrfProtection, (req, res) => {
|
|
||||||
res.render('blacklist', {csrfToken: req.csrfToken()});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/ajax/', (req, res) => {
|
|
||||||
let start = parseInt(req.body.start || 0, 10);
|
|
||||||
let limit = parseInt(req.body.length || 50, 10);
|
|
||||||
let search = req.body.search.value || '';
|
|
||||||
blacklist.get(start, limit, search, (err, data, total) => {
|
|
||||||
if (err) {
|
|
||||||
req.flash('danger', err.message || err);
|
|
||||||
return res.redirect('/');
|
|
||||||
}
|
|
||||||
res.json({
|
|
||||||
draw: req.body.draw,
|
|
||||||
recordsTotal: total,
|
|
||||||
recordsFiltered: total,
|
|
||||||
data: data.map((row, i) => [
|
|
||||||
(Number(req.body.start) || 0) + 1 + i,
|
|
||||||
htmlescape(row),
|
|
||||||
'<button class="btn btn-danger btn-sm" onclick="document.getElementById(\'delete-email-input\').value = \'' + row + '\'; document.getElementById(\'delete-email-form\').submit();">Delete</button>'
|
|
||||||
])
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/ajax/add', passport.csrfProtection, (req, res) => {
|
|
||||||
let email = req.body.email;
|
|
||||||
blacklist.add(email, (err) => {
|
|
||||||
if (err) {
|
|
||||||
req.flash('danger', err.message || err);
|
|
||||||
return res.redirect(req.body.next);
|
|
||||||
}
|
|
||||||
return res.redirect(req.body.next)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/ajax/delete', passport.csrfProtection, (req, res) => {
|
|
||||||
let email = req.body.email;
|
|
||||||
blacklist.delete(email, (err) => {
|
|
||||||
if (err) {
|
|
||||||
req.flash('danger', err.message || err);
|
|
||||||
return res.redirect(req.body.next);
|
|
||||||
}
|
|
||||||
return res.redirect(req.body.next);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -18,7 +18,7 @@ router.postAsync('/account', passport.loggedIn, passport.csrfProtection, async (
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
data.id = req.user.id;
|
data.id = req.user.id;
|
||||||
|
|
||||||
await users.updateWithConsistencyCheck(req.body, true);
|
await users.updateWithConsistencyCheck(contextHelpers.getAdminContext(), req.body, true);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ router.postAsync('/account-validate', passport.loggedIn, passport.csrfProtection
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
data.id = req.user.id;
|
data.id = req.user.id;
|
||||||
|
|
||||||
return res.json(await users.serverValidate(req.context, data, true));
|
return res.json(await users.serverValidate(contextHelpers.getAdminContext(), data, true));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/access-token', passport.loggedIn, async (req, res) => {
|
router.getAsync('/access-token', passport.loggedIn, async (req, res) => {
|
||||||
|
|
27
routes/rest/blacklist.js
Normal file
27
routes/rest/blacklist.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const passport = require('../../lib/passport');
|
||||||
|
const blacklist = require('../../models/blacklist');
|
||||||
|
|
||||||
|
const router = require('../../lib/router-async').create();
|
||||||
|
|
||||||
|
|
||||||
|
router.postAsync('/blacklist-table', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await blacklist.listDTAjax(req.context, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/blacklist', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
await blacklist.add(req.context, req.body.email);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.deleteAsync('/blacklist/:email', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
await blacklist.remove(req.context, req.params.email);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/blacklist-validate', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await blacklist.serverValidate(req.context, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -40,9 +40,4 @@ router.getAsync('/report-template-user-fields/:reportTemplateId', passport.logge
|
||||||
return res.json(userFields);
|
return res.json(userFields);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/report-templates-create-permitted', passport.loggedIn, async (req, res) => {
|
|
||||||
return res.json(await shares.checkTypePermission(req.context, 'namespace', 'createReportTemplate'));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
|
@ -70,7 +70,13 @@ class InvalidTokenError extends InteroperableError {
|
||||||
|
|
||||||
class DependencyNotFoundError extends InteroperableError {
|
class DependencyNotFoundError extends InteroperableError {
|
||||||
constructor(msg, data) {
|
constructor(msg, data) {
|
||||||
super('DependencyNotFound', msg, data);
|
super('DependencyNotFoundError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NamespaceNotFoundError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('NamespaceNotFoundError', msg, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,6 +100,7 @@ const errorTypes = {
|
||||||
IncorrectPasswordError,
|
IncorrectPasswordError,
|
||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
DependencyNotFoundError,
|
DependencyNotFoundError,
|
||||||
|
NamespaceNotFoundError,
|
||||||
PermissionDeniedError
|
PermissionDeniedError
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue