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 archive = require('./routes/archive');
|
||||
const api = require('./routes/api');
|
||||
const blacklist = require('./routes/blacklist');
|
||||
const editorapi = require('./routes/editorapi');
|
||||
const grapejs = require('./routes/grapejs');
|
||||
const mosaico = require('./routes/mosaico');
|
||||
|
@ -56,12 +55,14 @@ const fieldsRest = require('./routes/rest/fields');
|
|||
const sharesRest = require('./routes/rest/shares');
|
||||
const segmentsRest = require('./routes/rest/segments');
|
||||
const subscriptionsRest = require('./routes/rest/subscriptions');
|
||||
const blacklistRest = require('./routes/rest/blacklist');
|
||||
|
||||
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
||||
const usersLegacyIntegration = require('./routes/users-legacy-integration');
|
||||
const accountLegacyIntegration = require('./routes/account-legacy-integration');
|
||||
const reportsLegacyIntegration = require('./routes/reports-legacy-integration');
|
||||
const listsLegacyIntegration = require('./routes/lists-legacy-integration');
|
||||
const blacklistLegacyIntegration = require('./routes/blacklist-legacy-integration');
|
||||
|
||||
const interoperableErrors = require('./shared/interoperable-errors');
|
||||
|
||||
|
@ -235,7 +236,6 @@ app.use('/lists', lists);
|
|||
app.use('/templates', templates);
|
||||
app.use('/campaigns', campaigns);
|
||||
app.use('/settings', settings);
|
||||
app.use('/blacklist', blacklist);
|
||||
app.use('/links', links);
|
||||
app.use('/fields', fields);
|
||||
app.use('/forms', forms);
|
||||
|
@ -259,6 +259,7 @@ app.use('/users', usersLegacyIntegration);
|
|||
app.use('/namespaces', namespacesLegacyIntegration);
|
||||
app.use('/account', accountLegacyIntegration);
|
||||
app.use('/lists', listsLegacyIntegration);
|
||||
app.use('/blacklist', blacklistLegacyIntegration);
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/reports', reports);
|
||||
|
@ -281,6 +282,7 @@ app.use('/rest', fieldsRest);
|
|||
app.use('/rest', sharesRest);
|
||||
app.use('/rest', segmentsRest);
|
||||
app.use('/rest', subscriptionsRest);
|
||||
app.use('/rest', blacklistRest);
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/rest', reportTemplatesRest);
|
||||
|
|
|
@ -35,6 +35,8 @@
|
|||
"react-router-dom": "^4.1.1",
|
||||
"react-sortable-tree": "^1.2.0",
|
||||
"slugify": "^1.1.0",
|
||||
"react-dnd-html5-backend": "^2.4.1",
|
||||
"react-dnd-touch-backend": "^0.3.13",
|
||||
"url-parse": "^1.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -48,8 +50,6 @@
|
|||
"css-loader": "^0.28.4",
|
||||
"i18next-conv": "^3.0.3",
|
||||
"node-sass": "^4.5.3",
|
||||
"react-dnd-html5-backend": "^2.4.1",
|
||||
"react-dnd-touch-backend": "^0.3.13",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"url-loader": "^0.5.9",
|
||||
|
|
|
@ -19,28 +19,28 @@ const getStructure = t => {
|
|||
login: {
|
||||
title: t('Sign in'),
|
||||
link: '/account/login',
|
||||
component: Login,
|
||||
panelComponent: Login,
|
||||
},
|
||||
api: {
|
||||
title: t('API'),
|
||||
link: '/account/api',
|
||||
component: API
|
||||
panelComponent: API
|
||||
}
|
||||
};
|
||||
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
subPaths.forgot = {
|
||||
title: t('Password reset'),
|
||||
extraParams: [':username?'],
|
||||
link: '/account/forgot',
|
||||
component: Reset
|
||||
extraParams: [':username?'],
|
||||
link: '/account/forgot',
|
||||
panelComponent: Reset
|
||||
};
|
||||
|
||||
subPaths.reset = {
|
||||
title: t('Password reset'),
|
||||
extraParams: [':username', ':resetToken'],
|
||||
link: '/account/reset',
|
||||
component: ResetLink
|
||||
extraParams: [':username', ':resetToken'],
|
||||
link: '/account/reset',
|
||||
panelComponent: ResetLink
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -52,10 +52,9 @@ const getStructure = t => {
|
|||
account: {
|
||||
title: t('Account'),
|
||||
link: '/account',
|
||||
component: Account,
|
||||
panelComponent: Account,
|
||||
|
||||
children: subPaths
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +66,6 @@ export default function() {
|
|||
<I18nextProvider i18n={ i18n }><Section root='/account/login' structure={getStructure}/></I18nextProvider>,
|
||||
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 {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
family: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
family: 'glyphicon'
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
return <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) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
|
|
|
@ -10,13 +10,10 @@ import { withPageHelpers } from './page'
|
|||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import { TreeTable, TreeSelectMode } from './tree';
|
||||
import { Table, TableSelectMode } from './table';
|
||||
import { Button } from "./bootstrap-components";
|
||||
import {Button, Icon} from "./bootstrap-components";
|
||||
|
||||
import brace from 'brace';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/javascript';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/handlebars';
|
||||
import 'brace/theme/github';
|
||||
|
||||
import DayPicker from 'react-day-picker';
|
||||
|
@ -42,7 +39,8 @@ class Form extends Component {
|
|||
static propTypes = {
|
||||
stateOwner: PropTypes.object.isRequired,
|
||||
onSubmitAsync: PropTypes.func,
|
||||
format: PropTypes.string
|
||||
format: PropTypes.string,
|
||||
noStatus: PropTypes.bool
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
|
@ -60,7 +58,7 @@ class Form extends Component {
|
|||
const t = this.props.t;
|
||||
|
||||
const owner = this.props.stateOwner;
|
||||
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
if (this.props.onSubmitAsync) {
|
||||
|
@ -94,10 +92,10 @@ class Form extends Component {
|
|||
<fieldset disabled={owner.isFormDisabled()}>
|
||||
{props.children}
|
||||
</fieldset>
|
||||
{statusMessageText &&
|
||||
<AlignedRow htmlId="form-status-message">
|
||||
<p className={`alert alert-${statusMessageSeverity} ${styles.formStatus}`} role="alert">{statusMessageText}</p>
|
||||
</AlignedRow>
|
||||
{!props.noStatus && statusMessageText &&
|
||||
<AlignedRow htmlId="form-status-message">
|
||||
<p className={`alert alert-${statusMessageSeverity} ${styles.formStatus}`} role="alert">{statusMessageText}</p>
|
||||
</AlignedRow>
|
||||
}
|
||||
</form>
|
||||
);
|
||||
|
@ -107,18 +105,46 @@ class Form extends Component {
|
|||
|
||||
class Fieldset extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.string
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
formStateOwner: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
|
||||
const className = id ? owner.addFormValidationClass('', id) : '';
|
||||
|
||||
let helpBlock = null;
|
||||
if (this.props.help) {
|
||||
helpBlock = <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 (
|
||||
<fieldset>
|
||||
<fieldset className={className}>
|
||||
{props.label ? <legend>{props.label}</legend> : null}
|
||||
{props.children}
|
||||
<div className="fieldset-content">
|
||||
{props.children}
|
||||
{helpBlock}
|
||||
{validationBlock}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -411,6 +437,7 @@ class TextArea extends Component {
|
|||
}
|
||||
|
||||
|
||||
@translate()
|
||||
class DatePicker extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -426,7 +453,9 @@ class DatePicker extends Component {
|
|||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
format: PropTypes.string,
|
||||
birthday: PropTypes.bool,
|
||||
dateFormat: PropTypes.string
|
||||
dateFormat: PropTypes.string,
|
||||
formatDate: PropTypes.func,
|
||||
parseDate: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -447,7 +476,12 @@ class DatePicker extends Component {
|
|||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const props = this.props;
|
||||
owner.updateFormValue(id, props.birthday ? formatBirthday(props.dateFormat, date) : formatDate(props.dateFormat, date));
|
||||
|
||||
if (props.formatDate) {
|
||||
owner.updateFormValue(id, props.formatDate(date));
|
||||
} else {
|
||||
owner.updateFormValue(id, props.birthday ? formatBirthday(props.dateFormat, date) : formatDate(props.dateFormat, date));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
opened: false
|
||||
|
@ -459,6 +493,7 @@ class DatePicker extends Component {
|
|||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
const t = props.t;
|
||||
|
||||
function BirthdayPickerCaption({ date, localeUtils, onChange }) {
|
||||
const months = localeUtils.getMonths();
|
||||
|
@ -472,7 +507,15 @@ class DatePicker extends Component {
|
|||
let selectedDate, captionElement, fromMonth, toMonth, placeholder;
|
||||
const selectedDateStr = owner.getFormValue(id) || '';
|
||||
if (props.birthday) {
|
||||
selectedDate = parseBirthday(props.dateFormat, selectedDateStr);
|
||||
if (props.parseDate) {
|
||||
selectedDate = props.parseDate(selectedDateStr);
|
||||
if (selectedDate) {
|
||||
selectedDate = moment(selectedDate).set('year', birthdayYear).toDate();
|
||||
}
|
||||
} else {
|
||||
selectedDate = parseBirthday(props.dateFormat, selectedDateStr);
|
||||
}
|
||||
|
||||
if (!selectedDate) {
|
||||
selectedDate = moment().set('year', birthdayYear).toDate();
|
||||
}
|
||||
|
@ -483,7 +526,12 @@ class DatePicker extends Component {
|
|||
placeholder = getBirthdayFormatString(props.dateFormat);
|
||||
|
||||
} else {
|
||||
selectedDate = parseDate(props.dateFormat, selectedDateStr);
|
||||
if (props.parseDate) {
|
||||
selectedDate = props.parseDate(selectedDateStr);
|
||||
} else {
|
||||
selectedDate = parseDate(props.dateFormat, selectedDateStr);
|
||||
}
|
||||
|
||||
if (!selectedDate) {
|
||||
selectedDate = moment().toDate();
|
||||
}
|
||||
|
@ -495,19 +543,19 @@ class DatePicker extends Component {
|
|||
<div>
|
||||
<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)}/>
|
||||
<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>
|
||||
{this.state.opened &&
|
||||
<div className={styles.dayPickerWrapper}>
|
||||
<DayPicker
|
||||
onDayClick={date => this.daySelected(date)}
|
||||
selectedDays={selectedDate}
|
||||
initialMonth={selectedDate}
|
||||
fromMonth={fromMonth}
|
||||
toMonth={toMonth}
|
||||
captionElement={captionElement}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.dayPickerWrapper}>
|
||||
<DayPicker
|
||||
onDayClick={date => this.daySelected(date)}
|
||||
selectedDays={selectedDate}
|
||||
initialMonth={selectedDate}
|
||||
fromMonth={fromMonth}
|
||||
toMonth={toMonth}
|
||||
captionElement={captionElement}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
@ -729,7 +777,7 @@ class TableSelect extends Component {
|
|||
</span>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
|
@ -737,7 +785,7 @@ class TableSelect extends Component {
|
|||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||
<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>
|
||||
);
|
||||
|
@ -938,6 +986,7 @@ function withForm(target) {
|
|||
delete data.hash;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -951,6 +1000,7 @@ function withForm(target) {
|
|||
const data = this.getFormValues();
|
||||
|
||||
if (mutator) {
|
||||
// FIXME - change the interface such that the mutator is supposed to create the object to be submitted
|
||||
mutator(data);
|
||||
}
|
||||
|
||||
|
@ -1152,6 +1202,17 @@ function withForm(target) {
|
|||
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) {
|
||||
this.disableForm();
|
||||
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';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router';
|
||||
import {BrowserRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import { DismissibleAlert, Button } from './bootstrap-components';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import axios from '../lib/axios';
|
||||
import React, {Component} from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
import {withRouter} from "react-router";
|
||||
import {BrowserRouter as Router, Link, Redirect, Route, Switch} from "react-router-dom";
|
||||
import {withAsyncErrorHandler, withErrorHandling} from "./error-handling";
|
||||
import interoperableErrors from "../../../shared/interoperable-errors";
|
||||
import {Button, DismissibleAlert} from "./bootstrap-components";
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
import styles from "./styles.scss";
|
||||
|
||||
import {getRoutes, needsResolve, resolve, withPageHelpers} from "./page-common";
|
||||
|
||||
class Breadcrumb extends Component {
|
||||
static propTypes = {
|
||||
|
@ -163,28 +162,42 @@ class RouteContent extends Component {
|
|||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async resolve() {
|
||||
const route = this.props.route;
|
||||
|
||||
const keys = Object.keys(route.resolve);
|
||||
|
||||
if (keys.length > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
async resolve(props) {
|
||||
if (Object.keys(props.route.resolve).length === 0) {
|
||||
this.setState({
|
||||
resolved
|
||||
resolved: {}
|
||||
});
|
||||
|
||||
} else {
|
||||
this.setState({
|
||||
resolved: null
|
||||
});
|
||||
|
||||
const resolved = await resolve(props.route, props.match);
|
||||
|
||||
if (!this.disregardResolve) { // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
|
||||
this.setState({
|
||||
resolved
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -193,7 +206,7 @@ class RouteContent extends Component {
|
|||
const params = this.props.match.params;
|
||||
const resolved = this.state.resolved;
|
||||
|
||||
if (!route.render && !route.component && route.link) {
|
||||
if (!route.panelRender && !route.panelComponent && route.link) {
|
||||
let link;
|
||||
if (typeof route.link === 'function') {
|
||||
link = route.link(params);
|
||||
|
@ -211,11 +224,11 @@ class RouteContent extends Component {
|
|||
resolved
|
||||
};
|
||||
|
||||
let component;
|
||||
if (route.render) {
|
||||
component = route.render(compProps);
|
||||
} else if (route.component) {
|
||||
component = React.createElement(route.component, compProps, null);
|
||||
let panel;
|
||||
if (route.panelComponent) {
|
||||
panel = React.createElement(route.panelComponent, compProps);
|
||||
} else if (route.panelRender) {
|
||||
panel = route.panelRender(compProps);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -226,7 +239,7 @@ class RouteContent extends Component {
|
|||
<SecondaryNavBar className="visible-xs" route={route} params={params} resolved={resolved}/>
|
||||
</div>
|
||||
{this.props.flashMessage}
|
||||
{component}
|
||||
{panel}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -291,14 +304,6 @@ class SectionContent extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
getFlashMessageText() {
|
||||
return this.state.flashMessageText;
|
||||
}
|
||||
|
||||
getFlashMessageSeverity() {
|
||||
return this.state.flashMessageSeverity;
|
||||
}
|
||||
|
||||
setFlashMessage(severity, text) {
|
||||
this.setState({
|
||||
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) {
|
||||
let flashMessage;
|
||||
if (this.state.flashMessageText) {
|
||||
|
@ -429,7 +363,7 @@ class SectionContent extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
let routes = this.getRoutes('', {}, [], this.props.structure, []);
|
||||
let routes = getRoutes('', {}, [], this.props.structure, [], null, null);
|
||||
|
||||
return (
|
||||
<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) {
|
||||
const comp1 = withPageHelpers(target);
|
||||
|
||||
|
|
|
@ -102,6 +102,10 @@ class Table extends Component {
|
|||
}
|
||||
|
||||
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 count = this.selectionMap.size;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { DeleteModalDialog } from '../lib/modals';
|
|||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import { UnsubscriptionMode } from '../../../shared/lists';
|
||||
import styles from "../lib/styles.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -46,7 +47,7 @@ export default class CUD extends Component {
|
|||
default_form: null,
|
||||
public_subscribe: true,
|
||||
unsubscription_mode: UnsubscriptionMode.ONE_STEP,
|
||||
namespace: null
|
||||
namespace: mailtrainConfig.user.namespace
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +103,7 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
const unsubcriptionModeOptions = [
|
||||
{
|
||||
|
@ -146,7 +148,7 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{isEdit &&
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
|
@ -185,7 +187,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<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>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,8 @@ import validators from '../../../../shared/validators';
|
|||
import slugify from 'slugify';
|
||||
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
|
||||
import styles from "../../lib/styles.scss";
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/handlebars';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -113,10 +115,7 @@ export default class CUD extends Component {
|
|||
dateFormat: 'eur',
|
||||
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
|
||||
orderSubscribeBefore: 'end',
|
||||
orderManageBefore: 'end',
|
||||
orderListOptions: [],
|
||||
orderSubscribeOptions: [],
|
||||
orderManageOptions: []
|
||||
orderManageBefore: 'end'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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.'));
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
@ -459,7 +448,7 @@ export default class CUD extends Component {
|
|||
|
||||
{type !== 'option' &&
|
||||
<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="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>
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class List extends Component {
|
|||
const columns = [
|
||||
{ data: 4, title: "#" },
|
||||
{ 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: 3, title: t('Merge Tag') },
|
||||
|
|
|
@ -262,7 +262,7 @@ export default class CUD extends Component {
|
|||
name: '',
|
||||
description: '',
|
||||
selectedTemplate: 'layout',
|
||||
namespace: null
|
||||
namespace: mailtrainConfig.user.namespace
|
||||
};
|
||||
supplyDefaults(data);
|
||||
|
||||
|
@ -341,6 +341,7 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
const templateOptGroups = [];
|
||||
|
||||
|
@ -369,7 +370,7 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{isEdit &&
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
|
@ -433,7 +434,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<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>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -21,8 +21,6 @@ import Share from '../shares/Share';
|
|||
|
||||
|
||||
const getStructure = t => {
|
||||
const subPaths = {};
|
||||
|
||||
return {
|
||||
'': {
|
||||
title: t('Home'),
|
||||
|
@ -31,7 +29,7 @@ const getStructure = t => {
|
|||
'lists': {
|
||||
title: t('Lists'),
|
||||
link: '/lists',
|
||||
component: ListsList,
|
||||
panelComponent: ListsList,
|
||||
children: {
|
||||
':listId([0-9]+)': {
|
||||
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
|
||||
|
@ -47,7 +45,7 @@ const getStructure = t => {
|
|||
},
|
||||
link: params => `/lists/${params.listId}/subscriptions`,
|
||||
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: {
|
||||
':subscriptionId([0-9]+)': {
|
||||
title: resolved => resolved.subscription.email,
|
||||
|
@ -60,7 +58,7 @@ const getStructure = t => {
|
|||
':action(edit|delete)': {
|
||||
title: t('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: {
|
||||
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)': {
|
||||
title: t('Edit'),
|
||||
link: params => `/lists/${params.listId}/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: {
|
||||
title: t('Fields'),
|
||||
link: params => `/lists/${params.listId}/fields/`,
|
||||
visible: resolved => resolved.list.permissions.includes('manageFields'),
|
||||
render: props => <FieldsList list={props.resolved.list} />,
|
||||
panelRender: props => <FieldsList list={props.resolved.list} />,
|
||||
children: {
|
||||
':fieldId([0-9]+)': {
|
||||
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
|
||||
|
@ -95,7 +93,7 @@ const getStructure = t => {
|
|||
':action(edit|delete)': {
|
||||
title: t('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: {
|
||||
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'),
|
||||
link: params => `/lists/${params.listId}/segments`,
|
||||
visible: resolved => resolved.list.permissions.includes('manageSegments'),
|
||||
render: props => <SegmentsList list={props.resolved.list} />,
|
||||
panelRender: props => <SegmentsList list={props.resolved.list} />,
|
||||
children: {
|
||||
':segmentId([0-9]+)': {
|
||||
title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
|
||||
|
@ -125,7 +123,7 @@ const getStructure = t => {
|
|||
':action(edit|delete)': {
|
||||
title: t('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: {
|
||||
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'),
|
||||
link: params => `/lists/${params.listId}/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: {
|
||||
title: t('Create'),
|
||||
render: props => <ListsCUD action="create" />
|
||||
panelRender: props => <ListsCUD action="create" />
|
||||
},
|
||||
forms: {
|
||||
title: t('Custom Forms'),
|
||||
link: '/lists/forms',
|
||||
component: FormsList,
|
||||
panelComponent: FormsList,
|
||||
children: {
|
||||
':formsId([0-9]+)': {
|
||||
title: resolved => t('Custom Forms "{{name}}"', {name: resolved.forms.name}),
|
||||
|
@ -166,19 +164,19 @@ const getStructure = t => {
|
|||
title: t('Edit'),
|
||||
link: params => `/lists/forms/${params.formsId}/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: {
|
||||
title: t('Share'),
|
||||
link: params => `/lists/forms/${params.formsId}/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: {
|
||||
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>,
|
||||
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.'));
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,19 +64,19 @@ export default class List extends Component {
|
|||
@withAsyncErrorHandler
|
||||
async deleteSubscription(id) {
|
||||
await axios.delete(`/rest/subscriptions/${this.props.list.id}/${id}`);
|
||||
this.subscriptionsTable.refresh();
|
||||
this.blacklistTable.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async unsubscribeSubscription(id) {
|
||||
await axios.post(`/rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`);
|
||||
this.subscriptionsTable.refresh();
|
||||
this.blacklistTable.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async blacklistSubscription(id) {
|
||||
await axios.post(`/rest/XXX/${this.props.list.id}/${id}`); // FIXME - add url one the blacklist functionality is in
|
||||
this.subscriptionsTable.refresh();
|
||||
async blacklistSubscription(email) {
|
||||
await axios.post("/rest/blacklist", { email });
|
||||
this.blacklistTable.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -86,11 +86,11 @@ export default class List extends Component {
|
|||
|
||||
const columns = [
|
||||
{ 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() : '' }
|
||||
];
|
||||
|
||||
let colIdx = 5;
|
||||
let colIdx = 6;
|
||||
|
||||
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
|
||||
actions.push({
|
||||
label: <Icon icon="ban-circle" title={t('Blacklist')}/>,
|
||||
action: () => this.blacklistSubscription(data[0])
|
||||
});
|
||||
if (!data[5]) {
|
||||
actions.push({
|
||||
label: <Icon icon="ban-circle" title={t('Blacklist')}/>,
|
||||
action: () => this.blacklistSubscription(data[2])
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
label: <Icon icon="remove" title={t('Remove')}/>,
|
||||
|
@ -169,7 +170,7 @@ export default class List extends Component {
|
|||
</div>
|
||||
|
||||
|
||||
<Table ref={node => this.subscriptionsTable = node} withHeader dataUrl={dataUrl} columns={columns} />
|
||||
<Table ref={node => this.blacklistTable = node} withHeader dataUrl={dataUrl} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {SubscriptionStatus} from "../../../../shared/lists";
|
|||
import {ACEEditor, CheckBoxGroup, DatePicker, Dropdown, InputField, RadioGroup, TextArea} from "../../lib/form";
|
||||
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
||||
import {getFieldKey} from '../../../../shared/lists';
|
||||
import 'brace/mode/json';
|
||||
|
||||
export function getSubscriptionStatusLabels(t) {
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import axios from '../lib/axios';
|
|||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import {DeleteModalDialog} from "../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -34,10 +35,6 @@ export default class CUD extends Component {
|
|||
return this.props.entity && this.props.entity.id === 1; /* Global namespace id */
|
||||
}
|
||||
|
||||
isDelete() {
|
||||
return this.props.match.params.action === 'delete';
|
||||
}
|
||||
|
||||
removeNsIdSubtree(data) {
|
||||
for (let idx = 0; idx < data.length; idx++) {
|
||||
const entry = data[idx];
|
||||
|
@ -84,7 +81,7 @@ export default class CUD extends Component {
|
|||
this.populateFormValues({
|
||||
name: '',
|
||||
description: '',
|
||||
namespace: null
|
||||
namespace: mailtrainConfig.user.namespace
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -179,10 +176,11 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canDelete = isEdit && !this.isEditGlobal() && !this.hasChildren && this.props.entity.permissions.includes('delete');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!this.isEditGlobal() && !this.hasChildren && isEdit &&
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
|
@ -205,7 +203,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<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>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,7 @@ const getStructure = t => ({
|
|||
namespaces: {
|
||||
title: t('Namespaces'),
|
||||
link: '/namespaces',
|
||||
component: List,
|
||||
panelComponent: List,
|
||||
children: {
|
||||
':namespaceId([0-9]+)': {
|
||||
title: resolved => t('Namespace "{{name}}"', {name: resolved.namespace.name}),
|
||||
|
@ -31,19 +31,19 @@ const getStructure = t => ({
|
|||
title: t('Edit'),
|
||||
link: params => `/namespaces/${params.namespaceId}/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: {
|
||||
title: t('Share'),
|
||||
link: params => `/namespaces/${params.namespaceId}/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: {
|
||||
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>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'
|
|||
import moment from 'moment';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import {DeleteModalDialog} from "../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -65,7 +66,7 @@ export default class CUD extends Component {
|
|||
name: '',
|
||||
description: '',
|
||||
report_template: null,
|
||||
namespace: null,
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
user_fields: null
|
||||
});
|
||||
}
|
||||
|
@ -100,7 +101,7 @@ export default class CUD extends Component {
|
|||
const selection = state.getIn([fldId, 'value']) || [];
|
||||
|
||||
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'));
|
||||
}
|
||||
} else {
|
||||
|
@ -119,7 +120,7 @@ export default class CUD extends Component {
|
|||
async submitHandler() {
|
||||
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.'));
|
||||
return;
|
||||
}
|
||||
|
@ -160,9 +161,9 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
const reportTemplateColumns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() }
|
||||
|
@ -213,7 +214,7 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{isEdit &&
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
|
@ -246,7 +247,9 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<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>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@ const getStructure = t => {
|
|||
'reports': {
|
||||
title: t('Reports'),
|
||||
link: '/reports',
|
||||
component: ReportsList,
|
||||
panelComponent: ReportsList,
|
||||
children: {
|
||||
':reportId([0-9]+)': {
|
||||
title: resolved => t('Report "{{name}}"', {name: resolved.report.name}),
|
||||
|
@ -41,13 +41,13 @@ const getStructure = t => {
|
|||
title: t('Edit'),
|
||||
link: params => `/reports/${params.reportId}/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: {
|
||||
title: t('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',
|
||||
render: props => (<ReportsView {...props} />),
|
||||
panelRender: props => (<ReportsView {...props} />),
|
||||
},
|
||||
download: {
|
||||
title: t('Download'),
|
||||
|
@ -58,24 +58,24 @@ const getStructure = t => {
|
|||
title: t('Output'),
|
||||
link: params => `/reports/${params.reportId}/output`,
|
||||
visible: resolved => resolved.report.permissions.includes('viewOutput'),
|
||||
render: props => (<ReportsOutput {...props} />)
|
||||
panelRender: props => (<ReportsOutput {...props} />)
|
||||
},
|
||||
share: {
|
||||
title: t('Share'),
|
||||
link: params => `/reports/${params.reportId}/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: {
|
||||
title: t('Create'),
|
||||
render: props => <ReportsCUD action="create" />
|
||||
panelRender: props => <ReportsCUD action="create" />
|
||||
},
|
||||
'templates': {
|
||||
title: t('Templates'),
|
||||
link: '/reports/templates',
|
||||
component: ReportTemplatesList,
|
||||
panelComponent: ReportTemplatesList,
|
||||
children: {
|
||||
':templateId([0-9]+)': {
|
||||
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
|
||||
|
@ -88,20 +88,20 @@ const getStructure = t => {
|
|||
title: t('Edit'),
|
||||
link: params => `/reports/templates/${params.templateId}/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: {
|
||||
title: t('Share'),
|
||||
link: params => `/reports/templates/${params.templateId}/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: {
|
||||
title: t('Create'),
|
||||
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 { validateNamespace, NamespaceSelect } from '../../lib/namespace';
|
||||
import {DeleteModalDialog} from "../../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import 'brace/mode/javascript';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/handlebars';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -45,7 +49,7 @@ export default class CUD extends Component {
|
|||
this.populateFormValues({
|
||||
name: '',
|
||||
description: 'Generates a campaign report listing all subscribers along with their statistics.',
|
||||
namespace: null,
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
mime_type: 'text/html',
|
||||
user_fields:
|
||||
'[\n' +
|
||||
|
@ -95,7 +99,7 @@ export default class CUD extends Component {
|
|||
this.populateFormValues({
|
||||
name: '',
|
||||
description: 'Generates a campaign report with results are aggregated by some "Country" custom field.',
|
||||
namespace: null,
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
mime_type: 'text/html',
|
||||
user_fields:
|
||||
'[\n' +
|
||||
|
@ -166,7 +170,7 @@ export default class CUD extends Component {
|
|||
this.populateFormValues({
|
||||
name: '',
|
||||
description: 'Exports a list as a CSV file.',
|
||||
namespace: null,
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
mime_type: 'text/csv',
|
||||
user_fields:
|
||||
'[\n' +
|
||||
|
@ -191,7 +195,7 @@ export default class CUD extends Component {
|
|||
this.populateFormValues({
|
||||
name: '',
|
||||
description: '',
|
||||
namespace: null,
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
mime_type: 'text/html',
|
||||
user_fields: '',
|
||||
js: '',
|
||||
|
@ -270,10 +274,11 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isEdit &&
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
|
@ -299,7 +304,9 @@ export default class CUD extends Component {
|
|||
<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 Leave')}/>
|
||||
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/templates/${this.props.entity.id}/delete`}/>
|
||||
{canDelete &&
|
||||
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/templates/${this.props.entity.id}/delete`}/>
|
||||
}
|
||||
</ButtonRow>
|
||||
:
|
||||
<ButtonRow>
|
||||
|
|
|
@ -52,7 +52,7 @@ export default class CUD extends Component {
|
|||
email: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
namespace: null
|
||||
namespace: mailtrainConfig.user.namespace
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ export default class CUD extends Component {
|
|||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const userId = this.getFormValue('id');
|
||||
const canDelete = userId !== 1 && mailtrainConfig.userId !== userId;
|
||||
const canDelete = isEdit && userId !== 1 && mailtrainConfig.user.id !== userId;
|
||||
|
||||
const rolesColumns = [
|
||||
{ data: 1, title: "Name" },
|
||||
|
@ -192,7 +192,7 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{isEdit && canDelete &&
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
|
@ -220,7 +220,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<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>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
|
||||
import { Table } from '../lib/table';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import React, {Component} from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import {NavButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from "../lib/page";
|
||||
import {Table} from "../lib/table";
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
import {Icon} from "../lib/bootstrap-components";
|
||||
|
||||
@translate()
|
||||
|
|
|
@ -19,7 +19,7 @@ const getStructure = t => {
|
|||
users: {
|
||||
title: t('Users'),
|
||||
link: '/users',
|
||||
component: List,
|
||||
panelComponent: List,
|
||||
children: {
|
||||
':userId([0-9]+)': {
|
||||
title: resolved => t('User "{{name}}"', {name: resolved.user.name}),
|
||||
|
@ -31,18 +31,18 @@ const getStructure = t => {
|
|||
':action(edit|delete)': {
|
||||
title: t('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: {
|
||||
title: t('Shares'),
|
||||
link: params => `/users/${params.userId}/shares`,
|
||||
render: props => <UserShares user={props.resolved.user} />
|
||||
panelRender: props => <UserShares user={props.resolved.user} />
|
||||
}
|
||||
}
|
||||
},
|
||||
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>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@ module.exports = {
|
|||
users: ['babel-polyfill', './src/users/root.js'],
|
||||
account: ['babel-polyfill', './src/account/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: {
|
||||
library: 'MailtrainReactBody',
|
||||
|
|
|
@ -196,7 +196,7 @@ browser="phantomjs"
|
|||
name="Master"
|
||||
admin=true
|
||||
description="All permissions"
|
||||
permissions=["rebuildPermissions", "createJavascriptWithROAccess"]
|
||||
permissions=["rebuildPermissions", "createJavascriptWithROAccess", "manageBlacklist"]
|
||||
rootNamespaceRole="master"
|
||||
|
||||
[roles.namespace.master]
|
||||
|
|
|
@ -19,7 +19,10 @@ async function getAnonymousConfig(context) {
|
|||
async function getAuthenticatedConfig(context) {
|
||||
return {
|
||||
defaultCustomFormValues: await forms.getDefaultCustomFormValues(),
|
||||
userId: context.user.id,
|
||||
user: {
|
||||
id: context.user.id,
|
||||
namespace: context.user.namespace
|
||||
},
|
||||
globalPermissions: shares.getGlobalPermissions(context)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ async function ajaxListTx(tx, params, queryFun, columns, options) {
|
|||
const query = queryFun(tx);
|
||||
query.whereIn(columnsNames[parseInt(params.column)], params.values);
|
||||
query.select(columnsSelect);
|
||||
query.options({rowsAsArray:true});
|
||||
|
||||
const rows = await query;
|
||||
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) {
|
||||
enforce(entity.namespace, 'Entity namespace not set');
|
||||
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 => {
|
||||
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
|
||||
// We first construct the columns to contain all which is supposed to be show and extraColumns which contain
|
||||
// everything else that constitutes the subscription.
|
||||
|
@ -218,7 +220,14 @@ async function listDTAjax(context, listId, segmentId, params) {
|
|||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
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 = [];
|
||||
let listFldIdx = columns.length;
|
||||
const idxMap = {};
|
||||
|
@ -228,10 +237,10 @@ async function listDTAjax(context, listId, segmentId, params) {
|
|||
const fld = groupedFieldsMap[fldKey];
|
||||
|
||||
if (fld.column) {
|
||||
columns.push(fld.column);
|
||||
columns.push(listTable + '.' + fld.column);
|
||||
} else {
|
||||
columns.push({
|
||||
name: fldKey,
|
||||
name: listTable + '.' + fldKey,
|
||||
raw: 0
|
||||
})
|
||||
}
|
||||
|
@ -245,14 +254,14 @@ async function listDTAjax(context, listId, segmentId, params) {
|
|||
|
||||
if (fld.column) {
|
||||
if (!(fldKey in idxMap)) {
|
||||
extraColumns.push(fld.column);
|
||||
extraColumns.push(listTable + '.' + fld.column);
|
||||
idxMap[fldKey] = listFldIdx;
|
||||
listFldIdx += 1;
|
||||
}
|
||||
|
||||
} else {
|
||||
for (const optionColumn in fld.groupedOptions) {
|
||||
extraColumns.push(optionColumn);
|
||||
extraColumns.push(listTable + '.' + optionColumn);
|
||||
idxMap[optionColumn] = listFldIdx;
|
||||
listFldIdx += 1;
|
||||
}
|
||||
|
@ -265,7 +274,10 @@ async function listDTAjax(context, listId, segmentId, params) {
|
|||
tx,
|
||||
params,
|
||||
builder => {
|
||||
const query = builder.from(getTableName(listId));
|
||||
const query = builder
|
||||
.from(listTable)
|
||||
.leftOuterJoin('blacklist', listTable + '.email', 'blacklist.email')
|
||||
;
|
||||
query.where(function() {
|
||||
addSegmentQuery(this);
|
||||
});
|
||||
|
|
|
@ -267,6 +267,8 @@ async function getByUsernameIfPasswordMatch(username, password) {
|
|||
throw new interoperableErrors.IncorrectPasswordError();
|
||||
}
|
||||
|
||||
delete user.password;
|
||||
|
||||
return user;
|
||||
|
||||
} 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;
|
||||
data.id = req.user.id;
|
||||
|
||||
await users.updateWithConsistencyCheck(req.body, true);
|
||||
await users.updateWithConsistencyCheck(contextHelpers.getAdminContext(), req.body, true);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
@ -26,7 +26,7 @@ router.postAsync('/account-validate', passport.loggedIn, passport.csrfProtection
|
|||
const data = req.body;
|
||||
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) => {
|
||||
|
|
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);
|
||||
});
|
||||
|
||||
router.getAsync('/report-templates-create-permitted', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await shares.checkTypePermission(req.context, 'namespace', 'createReportTemplate'));
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -70,7 +70,13 @@ class InvalidTokenError extends InteroperableError {
|
|||
|
||||
class DependencyNotFoundError extends InteroperableError {
|
||||
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,
|
||||
InvalidTokenError,
|
||||
DependencyNotFoundError,
|
||||
NamespaceNotFoundError,
|
||||
PermissionDeniedError
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue