diff --git a/client/src/account/root.js b/client/src/account/root.js
index 8dca08f8..9e163bf6 100644
--- a/client/src/account/root.js
+++ b/client/src/account/root.js
@@ -31,14 +31,14 @@ const getStructure = t => {
if (mailtrainConfig.isAuthMethodLocal) {
subPaths.forgot = {
title: t('Password reset'),
- params: [':username?'],
+ extraParams: [':username?'],
link: '/account/forgot',
component: Reset
};
subPaths.reset = {
title: t('Password reset'),
- params: [':username', ':resetToken'],
+ extraParams: [':username', ':resetToken'],
link: '/account/reset',
component: ResetLink
};
diff --git a/client/src/lib/form.js b/client/src/lib/form.js
index 333c2174..e2494c9f 100644
--- a/client/src/lib/form.js
+++ b/client/src/lib/form.js
@@ -559,6 +559,7 @@ class ACEEditor extends Component {
}
}
+
function withForm(target) {
const inst = target.prototype;
@@ -672,20 +673,8 @@ function withForm(target) {
});
};
- inst.getFormValuesFromURL = async function(url, mutator) {
- setTimeout(() => {
- this.setState(previousState => {
- if (previousState.formState.get('state') === FormState.Loading) {
- return {
- formState: previousState.formState.set('state', FormState.LoadingWithNotice)
- };
- }
- });
- }, 500);
-
- const response = await axios.get(url);
-
- const data = response.data;
+ inst.getFormValuesFromEntity = function(entity, mutator) {
+ const data = Object.assign({}, entity);
data.originalHash = data.hash;
delete data.hash;
@@ -697,330 +686,6 @@ function withForm(target) {
this.populateFormValues(data);
};
- inst.validateAndSendFormValuesToURL = async function(method, url, mutator) {
- await this.waitForFormServerValidated();
-
- if (this.isFormWithoutErrors()) {
- const data = this.getFormValues();
-
- if (mutator) {
- mutator(data);
- }
-
- let response;
- if (method === FormSendMethod.PUT) {
- response = await axios.put(url, data);
- } else if (method === FormSendMethod.POST) {
- response = await axios.post(url, data);
- }
-
- return response.data || true;
-
- } else {
- this.showFormValidation();
- return false;
- }
- };
-
-
- inst.populateFormValues = function(data) {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- mutState.set('state', FormState.Ready);
-
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- for (const key in data) {
- mutStateData.set(key, Immutable.Map({
- value: data[key]
- }));
- }
- }));
-
- validateFormState(this, mutState);
- })
- }));
- };
-
- inst.waitForFormServerValidated = async function() {
- if (!this.isFormServerValidated()) {
- await new Promise(resolve => { formValidateResolve = resolve; });
- }
- };
-
- inst.scheduleFormRevalidate = function() {
- scheduleValidateForm(this);
- };
-
- inst.updateFormValue = function(key, value) {
- this.setState(previousState => {
- const oldValue = previousState.formState.getIn(['data', key, 'value']);
-
- let newState = {
- formState: previousState.formState.withMutations(mutState => {
- mutState.setIn(['data', key, 'value'], value);
- validateFormState(this, mutState);
- })
- };
-
- const onChangeCallbacks = this.state.formSettings.onChange || {};
-
- if (onChangeCallbacks[key]) {
- onChangeCallbacks[key](newState, key, oldValue, value);
- }
-
- return newState;
- });
- };
-
- inst.getFormValue = function(name) {
- return this.state.formState.getIn(['data', name, 'value']);
- };
-
- inst.getFormValues = function(name) {
- return this.state.formState.get('data').map(attr => attr.get('value')).toJS();
- };
-
- inst.getFormError = function(name) {
- return this.state.formState.getIn(['data', name, 'error']);
- };
-
- inst.isFormWithLoadingNotice = function() {
- return this.state.formState.get('state') === FormState.LoadingWithNotice;
- };
-
- inst.isFormLoading = function() {
- return this.state.formState.get('state') === FormState.Loading || this.state.formState.get('state') === FormState.LoadingWithNotice;
- };
-
- inst.isFormReady = function() {
- return this.state.formState.get('state') === FormState.Ready;
- };
-
- inst.isFormValidationShown = function() {
- return this.state.formState.get('isValidationShown');
- };
-
- inst.addFormValidationClass = function(className, name) {
- if (this.isFormValidationShown()) {
- const error = this.getFormError(name);
- if (error) {
- return className + ' has-error';
- } else {
- return className + ' has-success';
- }
- } else {
- return className;
- }
- };
-
- inst.getFormValidationMessage = function(name) {
- if (this.isFormValidationShown()) {
- return this.getFormError(name);
- } else {
- return '';
- }
- };
-
- inst.showFormValidation = function() {
- this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', true)}));
- };
-
- inst.hideFormValidation = function() {
- this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', false)}));
- };
-
- inst.isFormWithoutErrors = function() {
- return !this.state.formState.get('data').find(attr => attr.get('error'));
- };
-
- inst.isFormServerValidated = function() {
- return !this.state.formSettings.serverValidation || this.state.formSettings.serverValidation.changed.every(attr => this.state.formState.getIn(['data', attr, 'serverValidated']));
- };
-
- inst.getFormStatusMessageText = function() {
- return this.state.formState.get('statusMessageText');
- };
-
- inst.getFormStatusMessageSeverity = function() {
- return this.state.formState.get('statusMessageSeverity');
- };
-
- inst.setFormStatusMessage = function(severity, text) {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(map => {
- map.set('statusMessageText', text);
- map.set('statusMessageSeverity', severity);
- })
- }));
- };
-
- inst.clearFormStatusMessage = function() {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(map => {
- map.set('statusMessageText', '');
- })
- }));
- };
-
- inst.enableForm = function() {
- this.setState(previousState => ({formState: previousState.formState.set('isDisabled', false)}));
- };
-
- inst.disableForm = function() {
- this.setState(previousState => ({formState: previousState.formState.set('isDisabled', true)}));
- };
-
- inst.isFormDisabled = function() {
- return this.state.formState.get('isDisabled');
- };
-
- inst.formHandleChangedError = async function(fn) {
- const t = this.props.t;
- try {
- await fn();
- } catch (error) {
- if (error instanceof interoperableErrors.ChangedError) {
- this.disableForm();
- this.setFormStatusMessage('danger',
-
- {t('Your updates cannot be saved.')}{' '}
- {t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
-
- );
- return;
- }
-
- if (error instanceof interoperableErrors.NotFoundError) {
- this.disableForm();
- this.setFormStatusMessage('danger',
-
- {t('Your updates cannot be saved.')}{' '}
- {t('It seems that someone else has deleted the entity in the meantime.')}
-
- );
- return;
- }
-
- throw error;
- }
- };
-
- return target;
-}
-
-function withForm(target) {
- const inst = target.prototype;
-
- const cleanFormState = Immutable.Map({
- state: FormState.Loading,
- isValidationShown: false,
- isDisabled: false,
- statusMessageText: '',
- data: Immutable.Map(),
- isServerValidationRunning: false
- });
-
- // formValidateResolve is called by "validateForm" once client receives validation response from server that does not
- // trigger another server validation
- let formValidateResolve = null;
-
- function scheduleValidateForm(self) {
- setTimeout(() => {
- self.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- validateFormState(self, mutState);
- })
- }));
- }, 0);
- }
-
- function validateFormState(self, mutState) {
- const settings = self.state.formSettings;
-
- if (!mutState.get('isServerValidationRunning') && settings.serverValidation) {
- const payload = {};
- let payloadNotEmpty = false;
-
- for (const attr of settings.serverValidation.extra || []) {
- payload[attr] = mutState.getIn(['data', attr, 'value']);
- }
-
- for (const attr of settings.serverValidation.changed) {
- const currValue = mutState.getIn(['data', attr, 'value']);
- const serverValue = mutState.getIn(['data', attr, 'serverValue']);
-
- // This really assumes that all form values are preinitialized (i.e. not undef)
- if (currValue !== serverValue) {
- mutState.setIn(['data', attr, 'serverValidated'], false);
- payload[attr] = currValue;
- payloadNotEmpty = true;
- }
- }
-
- if (payloadNotEmpty) {
- mutState.set('isServerValidationRunning', true);
-
- axios.post(settings.serverValidation.url, payload)
- .then(response => {
-
- self.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- mutState.set('isServerValidationRunning', false);
-
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- for (const attr in payload) {
- mutStateData.setIn([attr, 'serverValue'], payload[attr]);
-
- if (payload[attr] === mutState.getIn(['data', attr, 'value'])) {
- mutStateData.setIn([attr, 'serverValidated'], true);
- mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true);
- }
- }
- }));
- })
- }));
-
- scheduleValidateForm(self);
- })
- .catch(error => {
- console.log('Error in "validateFormState": ' + error);
-
- self.setState(previousState => ({
- formState: previousState.formState.set('isServerValidationRunning', false)
- }));
-
- // TODO: It might be good not to give up immediatelly, but retry a couple of times
- // scheduleValidateForm(self);
- });
- } else {
- if (formValidateResolve) {
- const resolve = formValidateResolve;
- formValidateResolve = null;
- resolve();
- }
- }
- }
-
- if (self.localValidateFormValues) {
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- self.localValidateFormValues(mutStateData);
- }));
- }
- }
-
- inst.initForm = function(settings) {
- const state = this.state || {};
- state.formState = cleanFormState;
- state.formSettings = settings || {};
- this.state = state;
- };
-
- inst.resetFormState = function() {
- this.setState({
- formState: cleanFormState
- });
- };
-
inst.getFormValuesFromURL = async function(url, mutator) {
setTimeout(() => {
this.setState(previousState => {
diff --git a/client/src/lib/page.css b/client/src/lib/page.css
index 1dbf2b8f..28bfa709 100644
--- a/client/src/lib/page.css
+++ b/client/src/lib/page.css
@@ -43,4 +43,28 @@
h3.legend {
font-size: 21px;
margin-bottom: 20px;
+}
+
+.mt-secondary-nav {
+ margin-top: 5px;
+ margin-right: 5px;
+ text-align: right;
+}
+
+@media (max-width: 767px) {
+ .mt-secondary-nav {
+ margin: 0px;
+ background-color: #f5f5f5;
+ padding: 8px 5px;
+ border-radius: 4px;
+ }
+}
+
+.mt-secondary-nav > li {
+ display: inline-block;
+ float: none;
+}
+
+.mt-secondary-nav > li > a {
+ padding: 3px 10px;
}
\ No newline at end of file
diff --git a/client/src/lib/page.js b/client/src/lib/page.js
index fa48a796..5b069c68 100644
--- a/client/src/lib/page.js
+++ b/client/src/lib/page.js
@@ -4,120 +4,235 @@ 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 } from 'react-router-dom'
+import {BrowserRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom'
import './page.css';
-import { withErrorHandling } from './error-handling';
+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';
-class PageContent extends Component {
- static propTypes = {
- structure: PropTypes.object.isRequired
- }
-
- getRoutes(urlPrefix, children) {
- let routes = [];
- for (let routeKey in children) {
- const structure = children[routeKey];
-
- let path = urlPrefix + routeKey;
- let pathWithParams = path;
-
- if (structure.params) {
- pathWithParams = pathWithParams + '/' + structure.params.join('/');
- }
-
- if (structure.component || structure.render) {
- const route = {
- component: structure.component,
- render: structure.render,
- path: (pathWithParams === '' ? '/' : pathWithParams)
- };
-
- routes.push(route);
- }
-
- if (structure.children) {
- routes = routes.concat(this.getRoutes(path + '/', structure.children));
- }
- }
-
- return routes;
- }
-
- renderRoute(route) {
- if (route.component) {
- return
${data}
` },
{ data: 3, title: t('Subscribers') },
diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js
index 76b42d6d..19918ae4 100644
--- a/client/src/lists/fields/CUD.js
+++ b/client/src/lists/fields/CUD.js
@@ -26,17 +26,11 @@ export default class CUD extends Component {
this.state = {};
- this.state.listId = parseInt(props.match.params.listId);
-
- if (props.edit) {
- this.state.entityId = parseInt(props.match.params.fieldId);
- }
-
this.fieldTypes = getFieldTypes(props.t);
this.initForm({
serverValidation: {
- url: `/rest/fields-validate/${this.state.listId}`,
+ url: `/rest/fields-validate/${this.props.list.id}`,
changed: ['key'],
extra: ['id']
}
@@ -44,28 +38,21 @@ export default class CUD extends Component {
}
static propTypes = {
- edit: PropTypes.bool
- }
-
- @withAsyncErrorHandler
- async loadFormValues() {
- await this.getFormValuesFromURL(`/rest/fields/${this.state.listId}/${this.state.entityId}`, data => {
- if (data.default_value === null) {
- data.default_value = '';
- }
- });
+ action: PropTypes.string.isRequired,
+ list: PropTypes.object,
+ entity: PropTypes.object
}
@withAsyncErrorHandler
async loadOrderOptions() {
const t = this.props.t;
- const flds = await axios.get(`/rest/fields/${this.state.listId}`);
+ const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
- ...flds.data.filter(x => x.id !== this.state.entityId && x[fld] !== null).sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id, label: `${x.name} (${this.fieldTypes[x.type].label})`})),
+ ...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null).sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id, label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
];
};
@@ -78,8 +65,13 @@ export default class CUD extends Component {
}
componentDidMount() {
- if (this.props.edit) {
- this.loadFormValues();
+ if (this.props.entity) {
+ this.getFormValuesFromEntity(this.props.entity, data => {
+ if (data.default_value === null) {
+ data.default_value = '';
+ }
+ });
+
} else {
this.populateFormValues({
name: '',
@@ -101,7 +93,6 @@ export default class CUD extends Component {
localValidateFormValues(state) {
const t = this.props.t;
- const edit = this.props.edit;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
@@ -123,15 +114,14 @@ export default class CUD extends Component {
async submitHandler() {
const t = this.props.t;
- const edit = this.props.edit;
let sendMethod, url;
- if (edit) {
+ if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
- url = `/rest/fields/${this.state.listId}/${this.state.entityId}`
+ url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
- url = `/rest/fields/${this.state.listId}`
+ url = `/rest/fields/${this.props.list.id}`
}
try {
@@ -145,7 +135,7 @@ export default class CUD extends Component {
});
if (submitSuccessful) {
- this.navigateToWithFlashMessage(`/rest/fields/${this.state.listId}`, 'success', t('Field saved'));
+ this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
@@ -167,7 +157,7 @@ export default class CUD extends Component {
render() {
const t = this.props.t;
- const edit = this.props.edit;
+ const isEdit = !!this.props.entity;
/*
const orderColumns = [
@@ -183,18 +173,18 @@ export default class CUD extends Component {
return (