Fluid layout
Reworked routing and breadcrumb mechanism. It allows resolved parameters in paths, which allows including names of entities in the breadcrumb. Secondary navigation which is aware of permissions.
This commit is contained in:
parent
86fce404a9
commit
602364caae
33 changed files with 808 additions and 907 deletions
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{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.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof interoperableErrors.NotFoundError) {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('It seems that someone else has deleted the entity in the meantime.')}
|
||||
</span>
|
||||
);
|
||||
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 => {
|
||||
|
|
|
@ -44,3 +44,27 @@ 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;
|
||||
}
|
|
@ -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 <Route key={route.path} exact path={route.path} component={route.component} />;
|
||||
} else if (route.render) {
|
||||
return <Route key={route.path} exact path={route.path} render={route.render} />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let routes = this.getRoutes('', this.props.structure);
|
||||
return <Switch>{routes.map(x => this.renderRoute(x))}</Switch>;
|
||||
}
|
||||
}
|
||||
|
||||
@withRouter
|
||||
class Breadcrumb extends Component {
|
||||
static propTypes = {
|
||||
structure: PropTypes.object.isRequired
|
||||
route: PropTypes.object.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
resolved: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
renderElement(breadcrumbElem) {
|
||||
if (breadcrumbElem.isActive) {
|
||||
return <li key={breadcrumbElem.idx} className="active">{breadcrumbElem.title}</li>;
|
||||
renderElement(entry, isActive) {
|
||||
const params = this.props.params;
|
||||
let title;
|
||||
if (typeof entry.title === 'function') {
|
||||
title = entry.title(this.props.resolved);
|
||||
} else {
|
||||
title = entry.title;
|
||||
}
|
||||
|
||||
} else if (breadcrumbElem.externalLink) {
|
||||
return <li key={breadcrumbElem.idx}><a href={breadcrumbElem.externalLink}>{breadcrumbElem.title}</a></li>;
|
||||
if (isActive) {
|
||||
return <li key={entry.path} className="active">{title}</li>;
|
||||
|
||||
} else if (breadcrumbElem.link) {
|
||||
let link;
|
||||
if (typeof breadcrumbElem.link === 'function') {
|
||||
link = breadcrumbElem.link(this.props.match);
|
||||
} else if (entry.externalLink) {
|
||||
let externalLink;
|
||||
if (typeof entry.externalLink === 'function') {
|
||||
externalLink = entry.externalLink(params);
|
||||
} else {
|
||||
link = breadcrumbElem.link;
|
||||
externalLink = entry.externalLink;
|
||||
}
|
||||
return <li key={breadcrumbElem.idx}><Link to={link}>{breadcrumbElem.title}</Link></li>;
|
||||
|
||||
return <li key={entry.path}><a href={externalLink}>{title}</a></li>;
|
||||
|
||||
} else if (entry.link) {
|
||||
let link;
|
||||
if (typeof entry.link === 'function') {
|
||||
link = entry.link(params);
|
||||
} else {
|
||||
link = entry.link;
|
||||
}
|
||||
return <li key={entry.path}><Link to={link}>{title}</Link></li>;
|
||||
|
||||
} else {
|
||||
return <li key={breadcrumbElem.idx}>{breadcrumbElem.title}</li>;
|
||||
return <li key={entry.path}>{title}</li>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const location = this.props.location.pathname;
|
||||
const locationElems = location.split('/');
|
||||
const route = this.props.route;
|
||||
|
||||
let breadcrumbElems = [];
|
||||
let children = this.props.structure;
|
||||
|
||||
for (let idx = 0; idx < locationElems.length; idx++) {
|
||||
const breadcrumbElem = children[locationElems[idx]];
|
||||
if (!breadcrumbElem) {
|
||||
break;
|
||||
}
|
||||
|
||||
breadcrumbElem.isActive = (idx === locationElems.length - 1);
|
||||
breadcrumbElem.idx = idx;
|
||||
|
||||
breadcrumbElems.push(breadcrumbElem);
|
||||
children = breadcrumbElem.children;
|
||||
|
||||
if (!children) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const renderedElems = breadcrumbElems.map(x => this.renderElement(x));
|
||||
const renderedElems = [...route.parents.map(x => this.renderElement(x)), this.renderElement(route, true)];
|
||||
|
||||
return <ol className="breadcrumb">{renderedElems}</ol>;
|
||||
}
|
||||
}
|
||||
|
||||
class SecondaryNavBar extends Component {
|
||||
static propTypes = {
|
||||
route: PropTypes.object.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
resolved: PropTypes.object.isRequired,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
renderElement(key, entry) {
|
||||
const params = this.props.params;
|
||||
let title;
|
||||
if (typeof entry.title === 'function') {
|
||||
title = entry.title(this.props.resolved);
|
||||
} else {
|
||||
title = entry.title;
|
||||
}
|
||||
|
||||
let className = '';
|
||||
if (entry.active) {
|
||||
className += ' active';
|
||||
}
|
||||
|
||||
if (entry.link) {
|
||||
let link;
|
||||
|
||||
if (typeof entry.link === 'function') {
|
||||
link = entry.link(params);
|
||||
} else {
|
||||
link = entry.link;
|
||||
}
|
||||
|
||||
return <li key={key} role="presentation" className={className}><Link to={link}>{title}</Link></li>;
|
||||
|
||||
} else if (entry.externalLink) {
|
||||
let externalLink;
|
||||
if (typeof entry.externalLink === 'function') {
|
||||
externalLink = entry.externalLink(params);
|
||||
} else {
|
||||
externalLink = entry.externalLink;
|
||||
}
|
||||
|
||||
return <li key={key} role="presentation" className={className}><a href={externalLink}>{title}</a></li>;
|
||||
|
||||
} else {
|
||||
return <li key={key} role="presentation" className={className}>{title}</li>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const route = this.props.route;
|
||||
|
||||
const keys = Object.keys(route.navs);
|
||||
const renderedElems = [];
|
||||
|
||||
for (const key in keys) {
|
||||
const entry = route.navs[key];
|
||||
|
||||
let visible = true;
|
||||
if (typeof entry.visible === 'function') {
|
||||
visible = entry.visible(this.props.resolved);
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
renderedElems.push(this.renderElement(key, entry));
|
||||
}
|
||||
}
|
||||
|
||||
let className = 'mt-secondary-nav nav nav-pills';
|
||||
if (this.props.className) {
|
||||
className += ' ' + this.props.className;
|
||||
}
|
||||
|
||||
return <ul className={className}>{renderedElems}</ul>;
|
||||
}
|
||||
}
|
||||
|
||||
@translate()
|
||||
@withErrorHandling
|
||||
class RouteContent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
|
||||
if (Object.keys(props.route.resolve).length === 0) {
|
||||
this.state.resolved = {};
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
route: PropTypes.object.isRequired,
|
||||
flashMessage: PropTypes.object
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
resolved
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.resolve();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const route = this.props.route;
|
||||
const params = this.props.match.params;
|
||||
const resolved = this.state.resolved;
|
||||
|
||||
if (!route.render && !route.component && route.link) {
|
||||
let link;
|
||||
if (typeof route.link === 'function') {
|
||||
link = route.link(params);
|
||||
} else {
|
||||
link = route.link;
|
||||
}
|
||||
|
||||
return <Redirect to={link}/>;
|
||||
|
||||
} else {
|
||||
if (resolved) {
|
||||
const compProps = {
|
||||
match: this.props.match,
|
||||
location: this.props.location,
|
||||
resolved
|
||||
};
|
||||
|
||||
let component;
|
||||
if (route.render) {
|
||||
component = route.render(compProps);
|
||||
} else if (route.component) {
|
||||
component = React.createElement(route.component, compProps, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<SecondaryNavBar className="hidden-xs pull-right" route={route} params={params} resolved={resolved}/>
|
||||
<Breadcrumb route={route} params={params} resolved={resolved}/>
|
||||
<SecondaryNavBar className="visible-xs" route={route} params={params} resolved={resolved}/>
|
||||
</div>
|
||||
{this.props.flashMessage}
|
||||
{component}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div>{t('Loading...')}</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@withRouter
|
||||
|
@ -229,13 +344,93 @@ 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) {
|
||||
flashMessage = <DismissibleAlert severity={this.state.flashMessageSeverity} onCloseAsync={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>;
|
||||
}
|
||||
|
||||
const render = props => <RouteContent route={route} flashMessage={flashMessage} {...props}/>;
|
||||
|
||||
return <Route key={route.path} exact path={route.path} render={render} />
|
||||
}
|
||||
|
||||
render() {
|
||||
let routes = this.getRoutes('', {}, [], this.props.structure, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumb structure={this.props.structure} />
|
||||
{(this.state.flashMessageText && <DismissibleAlert severity={this.state.flashMessageSeverity} onCloseAsync={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>)}
|
||||
<PageContent structure={this.props.structure}/>
|
||||
</div>
|
||||
<Switch>{routes.map(x => this.renderRoute(x))}</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -280,9 +475,18 @@ class Title extends Component {
|
|||
}
|
||||
|
||||
class Toolbar extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
let className = 'pull-right mt-button-row';
|
||||
if (this.props.className) {
|
||||
className += ' ' + this.props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pull-right mt-button-row">
|
||||
<div className={className}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
|
||||
Dropdown, StaticField, CheckBox
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import { withErrorHandling } from '../lib/error-handling';
|
||||
import { DeleteModalDialog } from '../lib/delete';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import { UnsubscriptionMode } from '../../../shared/lists';
|
||||
|
@ -24,27 +24,19 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {};
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.id);
|
||||
}
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
edit: PropTypes.bool
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/lists/${this.state.entityId}`, data => {
|
||||
data.form = data.default_form ? 'custom' : 'default';
|
||||
});
|
||||
action: PropTypes.string.isRequired,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
data.form = data.default_form ? 'custom' : 'default';
|
||||
});
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
|
@ -60,7 +52,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'));
|
||||
|
@ -79,12 +70,11 @@ 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/lists/${this.state.entityId}`
|
||||
url = `/rest/lists/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/lists'
|
||||
|
@ -110,7 +100,7 @@ export default class CUD extends Component {
|
|||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
||||
const unsubcriptionModeOptions = [
|
||||
{
|
||||
|
@ -144,7 +134,7 @@ export default class CUD extends Component {
|
|||
key: 'custom',
|
||||
label: t('Custom Forms (select form below)')
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const customFormsColumns = [
|
||||
{data: 0, title: "#"},
|
||||
|
@ -155,23 +145,23 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
{isEdit &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/rest/lists/${this.state.entityId}`}
|
||||
cudUrl={`/lists/edit/${this.state.entityId}`}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/lists/${this.props.entity.id}`}
|
||||
cudUrl={`/lists/${this.props.entity.id}/edit`}
|
||||
listUrl="/lists"
|
||||
deletingMsg={t('Deleting list ...')}
|
||||
deletedMsg={t('List deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit List') : t('Create List')}</Title>
|
||||
<Title>{isEdit ? t('Edit List') : t('Create List')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
||||
{edit &&
|
||||
{isEdit &&
|
||||
<StaticField id="cid" label="List ID" help={t('This is the list ID displayed to the subscribers')}>
|
||||
{this.getFormValue('cid')}
|
||||
</StaticField>
|
||||
|
@ -184,7 +174,7 @@ export default class CUD extends Component {
|
|||
<Dropdown id="form" label={t('Forms')} options={formsOptions} help={t('Web and email forms and templates used in subscription management process.')}/>
|
||||
|
||||
{this.getFormValue('form') === 'custom' &&
|
||||
<TableSelect id="default_form" label={t('Custom Forms')} withHeader dropdown dataUrl='/rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans>The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.state.entityId}`}>here</a>.</Trans>}/>
|
||||
<TableSelect id="default_form" label={t('Custom Forms')} withHeader dropdown dataUrl='/rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans>The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.props.entity.id}`}>here</a>.</Trans>}/>
|
||||
}
|
||||
|
||||
<CheckBox id="public_subscribe" label={t('Subscription')} text={t('Allow public users to subscribe themselves')}/>
|
||||
|
@ -194,7 +184,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/edit/${this.state.entityId}/delete`}/>}
|
||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.entity.id}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -48,21 +48,21 @@ export default class List extends Component {
|
|||
if (perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: '/lists/edit/' + data[0]
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
link: '/lists/share/' + data[0]
|
||||
link: `/lists/${data[0]}/edit`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('manageFields')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-th-list" aria-hidden="true" title="Manage Fields"></span>,
|
||||
link: '/lists/fields/' + data[0]
|
||||
link: `/lists/${data[0]}/fields`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
link: `/lists/${data[0]}/share`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,6 @@ export default class List extends Component {
|
|||
};
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('ID'), render: data => `<code>${data}</code>` },
|
||||
{ data: 3, title: t('Subscribers') },
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
{edit &&
|
||||
{isEdit &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/rest/fields/${this.state.listId}/${this.state.entityId}`}
|
||||
cudUrl={`/lists/fields/edit/${this.state.listId}/${this.state.entityId}`}
|
||||
listUrl={`/lists/fields/${this.state.listId}`}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`}
|
||||
cudUrl={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/edit`}
|
||||
listUrl={`/lists/fields/${this.props.list.id}`}
|
||||
deletingMsg={t('Deleting field ...')}
|
||||
deletedMsg={t('Field deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit Field') : t('Create Field')}</Title>
|
||||
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
@ -215,7 +205,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/edit/${this.state.listId}/${this.state.entityId}/delete`}/>}
|
||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class List extends Component {
|
|||
|
||||
const actions = data => [{
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: `/lists/fields/edit/${this.state.listId}/${data[0]}`
|
||||
link: `/lists/${this.state.listId}/fields/${data[0]}/edit`
|
||||
}];
|
||||
|
||||
const columns = [
|
||||
|
@ -42,7 +42,7 @@ export default class List extends Component {
|
|||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<NavButton linkTo={`/lists/fields/${this.state.listId}/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
|
||||
<NavButton linkTo={`/lists/${this.state.listId}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
|
||||
</Toolbar>
|
||||
|
||||
<Title>{t('Fields')}</Title>
|
||||
|
|
|
@ -24,10 +24,6 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {};
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.id);
|
||||
}
|
||||
|
||||
this.serverValidatedFields = [
|
||||
'layout',
|
||||
'web_subscribe',
|
||||
|
@ -241,11 +237,12 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
static propTypes = {
|
||||
edit: PropTypes.bool
|
||||
action: PropTypes.string.isRequired,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadOrPopulateFormValues() {
|
||||
|
||||
componentDidMount() {
|
||||
function supplyDefaults(data) {
|
||||
for (const key in mailtrainConfig.defaultCustomFormValues) {
|
||||
if (!data[key]) {
|
||||
|
@ -254,11 +251,12 @@ export default class CUD extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.props.edit) {
|
||||
await this.getFormValuesFromURL(`/rest/forms/${this.state.entityId}`, data => {
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
data.selectedTemplate = 'layout';
|
||||
supplyDefaults(data);
|
||||
});
|
||||
|
||||
} else {
|
||||
const data = {
|
||||
name: '',
|
||||
|
@ -272,13 +270,8 @@ export default class CUD extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadOrPopulateFormValues();
|
||||
}
|
||||
|
||||
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'));
|
||||
|
@ -319,12 +312,11 @@ 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/forms/${this.state.entityId}`
|
||||
url = `/rest/forms/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/forms'
|
||||
|
@ -348,7 +340,7 @@ export default class CUD extends Component {
|
|||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
||||
const templateOptGroups = [];
|
||||
|
||||
|
@ -377,18 +369,18 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
{isEdit &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/rest/forms/${this.state.entityId}`}
|
||||
cudUrl={`/lists/forms/edit/${this.state.entityId}`}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/forms/${this.props.entity.id}`}
|
||||
cudUrl={`/lists/forms/${this.props.entity.id}/edit`}
|
||||
listUrl="/lists/forms"
|
||||
deletingMsg={t('Deleting form ...')}
|
||||
deletedMsg={t('Form deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit Custom Forms') : t('Create Custom Forms')}</Title>
|
||||
<Title>{isEdit ? t('Edit Custom Forms') : t('Create Custom Forms')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
@ -441,7 +433,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/forms/edit/${this.state.entityId}/delete`}/>}
|
||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/forms/${this.props.entity.id}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -48,13 +48,13 @@ export default class List extends Component {
|
|||
if (perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: '/lists/forms/edit/' + data[0]
|
||||
link: `/lists/forms/${data[0]}/edit`
|
||||
});
|
||||
}
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
link: '/lists/forms/share/' + data[0]
|
||||
link: `/lists/forms/${data[0]}/share`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,6 @@ export default class List extends Component {
|
|||
};
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('Namespace') }
|
||||
|
|
|
@ -28,79 +28,86 @@ const getStructure = t => {
|
|||
link: '/lists',
|
||||
component: ListsList,
|
||||
children: {
|
||||
/* FIXME
|
||||
':listId': {
|
||||
':listId([0-9]+)': {
|
||||
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
|
||||
resolve: {
|
||||
list: match => `/rest/lists/${match.params.listId}`
|
||||
list: params => `/rest/lists/${params.listId}`
|
||||
},
|
||||
actions: {
|
||||
edit: {
|
||||
link: params => `/lists/${params.listId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
title: t('Edit'),
|
||||
params: [':action?'],
|
||||
render: props => (<ListsCUD edit entity={resolved.list} {...props} />)
|
||||
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} />
|
||||
},
|
||||
create: {
|
||||
title: t('Create'),
|
||||
render: props => (<ListsCUD entity={resolved.list} {...props} />)
|
||||
fields: {
|
||||
title: t('Fields'),
|
||||
link: params => `/lists/${params.listId}/fields/`,
|
||||
visible: resolved => resolved.list.permissions.includes('manageFields'),
|
||||
component: FieldsList,
|
||||
children: {
|
||||
':fieldId([0-9]+)': {
|
||||
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
|
||||
resolve: {
|
||||
field: params => `/rest/fields/${params.listId}/${params.fieldId}`
|
||||
},
|
||||
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
|
||||
navs: {
|
||||
':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} />
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
title: t('Create Field'),
|
||||
render: props => <FieldsCUD action="create" list={props.resolved.list} />
|
||||
}
|
||||
}
|
||||
},
|
||||
share: {
|
||||
title: t('Share'),
|
||||
render: props => (<Share title={t('Share')} entity={resolved.list} entityTypeId="list" {...props} />)
|
||||
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" />
|
||||
}
|
||||
}
|
||||
},
|
||||
*/
|
||||
edit: {
|
||||
title: t('Edit List'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<ListsCUD edit {...props} />)
|
||||
},
|
||||
create: {
|
||||
title: t('Create List'),
|
||||
render: props => (<ListsCUD {...props} />)
|
||||
},
|
||||
share: {
|
||||
title: t('Share List'),
|
||||
params: [':id'],
|
||||
render: props => (<Share title={entity => t('Share List "{{name}}"', {name: entity.name})} getUrl={id => `/rest/lists/${id}`} entityTypeId="list" {...props} />)
|
||||
},
|
||||
fields: {
|
||||
title: t('Fields'),
|
||||
params: [':listId'],
|
||||
link: match => `/lists/fields/${match.params.listId}`,
|
||||
component: FieldsList,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit Field'),
|
||||
params: [':listId', ':fieldId', ':action?'],
|
||||
render: props => (<FieldsCUD edit {...props} />)
|
||||
},
|
||||
create: {
|
||||
title: t('Create Field'),
|
||||
params: [':listId'],
|
||||
render: props => (<FieldsCUD {...props} />)
|
||||
},
|
||||
}
|
||||
title: t('Create'),
|
||||
render: props => <ListsCUD action="create" />
|
||||
},
|
||||
forms: {
|
||||
title: t('Custom Forms'),
|
||||
link: '/lists/forms',
|
||||
component: FormsList,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit Custom Forms'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<FormsCUD edit {...props} />)
|
||||
':formsId([0-9]+)': {
|
||||
title: resolved => t('Custom Forms "{{name}}"', {name: resolved.forms.name}),
|
||||
resolve: {
|
||||
forms: params => `/rest/forms/${params.formsId}`
|
||||
},
|
||||
link: params => `/lists/forms/${params.formsId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
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} />
|
||||
},
|
||||
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" />
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
title: t('Create Custom Forms'),
|
||||
render: props => (<FormsCUD {...props} />)
|
||||
},
|
||||
share: {
|
||||
title: t('Share Custom Forms'),
|
||||
params: [':id'],
|
||||
render: props => (<Share title={entity => t('Custom Forms "{{name}}"', {name: entity.name})} getUrl={id => `/rest/forms/${id}`} entityTypeId="customForm" {...props} />)
|
||||
title: t('Create'),
|
||||
render: props => <FormsCUD action="create" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,21 +21,17 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {};
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.id);
|
||||
}
|
||||
|
||||
this.initForm();
|
||||
this.hasChildren = false;
|
||||
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
edit: PropTypes.bool
|
||||
action: PropTypes.string.isRequired,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
isEditGlobal() {
|
||||
return this.state.entityId === 1; /* Global namespace id */
|
||||
return this.props.entity && this.props.entity.id === 1; /* Global namespace id */
|
||||
}
|
||||
|
||||
isDelete() {
|
||||
|
@ -46,7 +42,7 @@ export default class CUD extends Component {
|
|||
for (let idx = 0; idx < data.length; idx++) {
|
||||
const entry = data[idx];
|
||||
|
||||
if (entry.key === this.state.entityId) {
|
||||
if (entry.key === this.props.entity.id) {
|
||||
if (entry.children.length > 0) {
|
||||
this.hasChildren = true;
|
||||
}
|
||||
|
@ -71,7 +67,7 @@ export default class CUD extends Component {
|
|||
root.expanded = true;
|
||||
}
|
||||
|
||||
if (this.props.edit && !this.isEditGlobal()) {
|
||||
if (this.props.entity && !this.isEditGlobal()) {
|
||||
this.removeNsIdSubtree(data);
|
||||
}
|
||||
|
||||
|
@ -81,14 +77,9 @@ export default class CUD extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/namespaces/${this.state.entityId}`);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity);
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
|
@ -122,12 +113,11 @@ 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/namespaces/${this.state.entityId}`
|
||||
url = `/rest/namespaces/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/namespaces'
|
||||
|
@ -188,23 +178,23 @@ export default class CUD extends Component {
|
|||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!this.isEditGlobal() && !this.hasChildren && edit &&
|
||||
{!this.isEditGlobal() && !this.hasChildren && isEdit &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/rest/namespaces/${this.state.entityId}`}
|
||||
cudUrl={`/namespaces/edit/${this.state.entityId}`}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/namespaces/${this.props.entity.id}`}
|
||||
cudUrl={`/namespaces/${this.props.entity.id}/edit`}
|
||||
listUrl="/namespaces"
|
||||
deletingMsg={t('Deleting namespace ...')}
|
||||
deletedMsg={t('Namespace deleted')}
|
||||
onErrorAsync={::this.onDeleteError}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit Namespace') : t('Create Namespace')}</Title>
|
||||
<Title>{isEdit ? t('Edit Namespace') : t('Create Namespace')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
@ -215,7 +205,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{!this.isEditGlobal() && !this.hasChildren && edit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/namespaces/edit/${this.state.entityId}/delete`}/>}
|
||||
{!this.isEditGlobal() && !this.hasChildren && isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/namespaces/${this.props.entity.id}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -4,13 +4,38 @@ import React, { Component } from 'react';
|
|||
import { translate } from 'react-i18next';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
|
||||
import { TreeTable } from '../lib/tree';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import axios from '../lib/axios';
|
||||
|
||||
@translate()
|
||||
@withErrorHandling
|
||||
@withPageHelpers
|
||||
@requiresAuthenticatedUser
|
||||
export default class List extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
createNamespace: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createNamespace']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createNamespace
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchPermissions();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -21,15 +46,15 @@ export default class List extends Component {
|
|||
|
||||
if (node.data.permissions.includes('edit')) {
|
||||
actions.push({
|
||||
label: 'Edit',
|
||||
link: '/namespaces/edit/' + node.key
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: `/namespaces/${node.key}/edit`
|
||||
});
|
||||
}
|
||||
|
||||
if (node.data.permissions.includes('share')) {
|
||||
actions.push({
|
||||
label: 'Share',
|
||||
link: '/namespaces/share/' + node.key
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
link: `/namespaces/${node.key}/share`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -38,9 +63,11 @@ export default class List extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<NavButton linkTo="/namespaces/create" className="btn-primary" icon="plus" label={t('Create Namespace')}/>
|
||||
</Toolbar>
|
||||
{this.state.createPermitted &&
|
||||
<Toolbar>
|
||||
<NavButton linkTo="/namespaces/create" className="btn-primary" icon="plus" label={t('Create Namespace')}/>
|
||||
</Toolbar>
|
||||
}
|
||||
|
||||
<Title>{t('Namespaces')}</Title>
|
||||
|
||||
|
|
|
@ -20,20 +20,31 @@ const getStructure = t => ({
|
|||
link: '/namespaces',
|
||||
component: List,
|
||||
children: {
|
||||
edit : {
|
||||
title: t('Edit Namespace'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<CUD edit {...props} />)
|
||||
':namespaceId([0-9]+)': {
|
||||
title: resolved => t('Namespace "{{name}}"', {name: resolved.namespace.name}),
|
||||
resolve: {
|
||||
namespace: params => `/rest/namespaces/${params.namespaceId}`
|
||||
},
|
||||
link: params => `/namespaces/${params.namespaceId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
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} />
|
||||
},
|
||||
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" />
|
||||
}
|
||||
}
|
||||
},
|
||||
create : {
|
||||
title: t('Create Namespace'),
|
||||
render: props => (<CUD {...props} />)
|
||||
create: {
|
||||
title: t('Create'),
|
||||
render: props => <CUD action="create" />
|
||||
},
|
||||
share: {
|
||||
title: t('Share Namespace'),
|
||||
params: [':id'],
|
||||
render: props => (<Share title={entity => t('Share Namespace "{{name}}"', {name: entity.name})} getUrl={id => `/rest/namespaces/${id}`} entityTypeId="namespace" {...props} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,6 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {};
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.id);
|
||||
}
|
||||
|
||||
this.initForm({
|
||||
onChange: {
|
||||
report_template: ::this.onReportTemplateChange
|
||||
|
@ -37,11 +33,8 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
static propTypes = {
|
||||
edit: PropTypes.bool
|
||||
}
|
||||
|
||||
isDelete() {
|
||||
return this.props.match.params.action === 'delete';
|
||||
action: PropTypes.string.isRequired,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
|
@ -60,18 +53,13 @@ export default class CUD extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/reports/${this.state.entityId}`, data => {
|
||||
for (const key in data.params) {
|
||||
data[`param_${key}`] = data.params[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
for (const key in data.params) {
|
||||
data[`param_${key}`] = data.params[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
|
@ -85,7 +73,7 @@ export default class CUD extends Component {
|
|||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const edit = this.props.entity;
|
||||
|
||||
if (!state.getIn(['name', 'value'])) {
|
||||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||
|
@ -130,7 +118,6 @@ export default class CUD extends Component {
|
|||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
if (!this.getFormValue('user_fields')) {
|
||||
this.setFormStatusMessage('warning', t('Report parameters are not selected. Wait for them to get displayed and then fill them in.'));
|
||||
|
@ -138,9 +125,9 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
let sendMethod, url;
|
||||
if (edit) {
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/reports/${this.state.entityId}`
|
||||
url = `/rest/reports/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/reports'
|
||||
|
@ -172,7 +159,7 @@ export default class CUD extends Component {
|
|||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
||||
const reportTemplateColumns = [
|
||||
{ data: 0, title: "#" },
|
||||
|
@ -226,18 +213,18 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
{isEdit &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/reports/${this.state.entityId}`}
|
||||
cudUrl={`/reports/edit/${this.state.entityId}`}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/reports/${this.props.entity.id}`}
|
||||
cudUrl={`/reports/${this.props.entity.id}/edit`}
|
||||
listUrl="/reports"
|
||||
deletingMsg={t('Deleting report ...')}
|
||||
deletedMsg={t('Report deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit Report') : t('Create Report')}</Title>
|
||||
<Title>{isEdit ? t('Edit Report') : t('Create Report')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
@ -259,7 +246,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/edit/${this.state.entityId}/delete`}/>}
|
||||
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/${this.props.entity.id}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -94,12 +94,12 @@ export default class List extends Component {
|
|||
if (mimeType === 'text/html') {
|
||||
viewContent = {
|
||||
label: <span className="glyphicon glyphicon-eye-open" aria-hidden="true" title="View"></span>,
|
||||
link: `reports/view/${id}`
|
||||
link: `/reports/${id}/view`
|
||||
};
|
||||
} else if (mimeType === 'text/csv') {
|
||||
viewContent = {
|
||||
label: <span className="glyphicon glyphicon-download-alt" aria-hidden="true" title="Download"></span>,
|
||||
href: `reports/download/${id}`
|
||||
href: `/reports/${id}/download`
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ export default class List extends Component {
|
|||
actions.push(
|
||||
{
|
||||
label: <span className="glyphicon glyphicon-modal-window" aria-hidden="true" title="View console output"></span>,
|
||||
link: `reports/output/${id}`
|
||||
link: `/reports/${id}/output`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -139,14 +139,14 @@ export default class List extends Component {
|
|||
if (perms.includes('edit') && permsReportTemplate.includes('execute')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: `/reports/edit/${id}`
|
||||
link: `/reports/${id}/edit`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
link: `/reports/share/${id}`
|
||||
link: `/reports/${id}/share`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,6 @@ export default class List extends Component {
|
|||
};
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Template') },
|
||||
{ data: 3, title: t('Description') },
|
||||
|
|
|
@ -13,6 +13,7 @@ import ReportsOutput from './Output';
|
|||
import ReportTemplatesCUD from './templates/CUD';
|
||||
import ReportTemplatesList from './templates/List';
|
||||
import Share from '../shares/Share';
|
||||
import { ReportState } from '../../../shared/reports';
|
||||
|
||||
|
||||
const getStructure = t => {
|
||||
|
@ -28,49 +29,78 @@ const getStructure = t => {
|
|||
link: '/reports',
|
||||
component: ReportsList,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit Report'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<ReportsCUD edit {...props} />)
|
||||
':reportId([0-9]+)': {
|
||||
title: resolved => t('Report "{{name}}"', {name: resolved.report.name}),
|
||||
resolve: {
|
||||
report: params => `/rest/reports/${params.reportId}`
|
||||
},
|
||||
link: params => `/reports/${params.reportId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
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} />
|
||||
},
|
||||
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} />),
|
||||
},
|
||||
download: {
|
||||
title: t('Download'),
|
||||
externalLink: params => `/reports/${params.reportId}/download`,
|
||||
visible: resolved => resolved.report.permissions.includes('viewContent') && resolved.report.state === ReportState.FINISHED && resolved.report.mime_type === 'text/csv'
|
||||
},
|
||||
output: {
|
||||
title: t('Output'),
|
||||
link: params => `/reports/${params.reportId}/output`,
|
||||
visible: resolved => resolved.report.permissions.includes('viewOutput'),
|
||||
render: 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" />
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
title: t('Create Report'),
|
||||
render: props => (<ReportsCUD {...props} />)
|
||||
},
|
||||
view: {
|
||||
title: t('View Report'),
|
||||
params: [':id' ],
|
||||
render: props => (<ReportsView {...props} />)
|
||||
},
|
||||
output: {
|
||||
title: t('View Report Output'),
|
||||
params: [':id' ],
|
||||
render: props => (<ReportsOutput {...props} />)
|
||||
},
|
||||
share: {
|
||||
title: t('Share Report'),
|
||||
params: [':id'],
|
||||
render: props => (<Share title={entity => t('Share Report "{{name}}"', {name: entity.name})} getUrl={id => `/rest/reports/${id}`} entityTypeId="report" {...props} />)
|
||||
title: t('Create'),
|
||||
render: props => <ReportsCUD action="create" />
|
||||
},
|
||||
'templates': {
|
||||
title: t('Templates'),
|
||||
link: '/reports/templates',
|
||||
component: ReportTemplatesList,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit Report Template'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<ReportTemplatesCUD edit {...props} />)
|
||||
':templateId([0-9]+)': {
|
||||
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
|
||||
resolve: {
|
||||
template: params => `/rest/report-templates/${params.templateId}`
|
||||
},
|
||||
link: params => `/reports/templates/${params.templateId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
title: t('Edit'),
|
||||
link: params => `/reports/templates/${params.templateId}/edit`,
|
||||
visible: resolved => resolved.template.permissions.includes('edit'),
|
||||
render: 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" />
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
title: t('Create Report Template'),
|
||||
params: [':wizard?'],
|
||||
render: props => (<ReportTemplatesCUD {...props} />)
|
||||
},
|
||||
share: {
|
||||
title: t('Share Report Template'),
|
||||
params: [':id'],
|
||||
render: props => (<Share title={entity => t('Share Report Template "{{name}}"', {name: entity.name})} getUrl={id => `/rest/report-templates/${id}`} entityTypeId="reportTemplate" {...props} />)
|
||||
title: t('Create'),
|
||||
extraParams: [':wizard?'],
|
||||
render: props => <ReportTemplatesCUD action="create" wizard={props.match.params.wizard} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,32 +20,21 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {};
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.id);
|
||||
}
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
edit: PropTypes.bool
|
||||
}
|
||||
|
||||
isDelete() {
|
||||
return this.props.match.params.action === 'delete';
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/report-templates/${this.state.entityId}`);
|
||||
action: PropTypes.string.isRequired,
|
||||
wizard: PropTypes.string,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity);
|
||||
|
||||
} else {
|
||||
const wizard = this.props.match.params.wizard;
|
||||
const wizard = this.props.wizard;
|
||||
|
||||
if (wizard === 'subscribers-all') {
|
||||
this.populateFormValues({
|
||||
|
@ -209,7 +198,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'));
|
||||
|
@ -245,12 +233,11 @@ export default class CUD extends Component {
|
|||
|
||||
async doSubmit(stay) {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
let sendMethod, url;
|
||||
if (edit) {
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/report-templates/${this.state.entityId}`
|
||||
url = `/rest/report-templates/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/report-templates'
|
||||
|
@ -277,22 +264,22 @@ export default class CUD extends Component {
|
|||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
{isEdit &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/reports/templates/${this.state.entityId}`}
|
||||
cudUrl={`/reports/templates/edit/${this.state.entityId}`}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/reports/templates/${this.props.entity.id}`}
|
||||
cudUrl={`/reports/templates/${this.props.entity.id}/edit`}
|
||||
listUrl="/reports/templates"
|
||||
deletingMsg={t('Deleting report template ...')}
|
||||
deletedMsg={t('Report template deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit Report Template') : t('Create Report Template')}</Title>
|
||||
<Title>{isEdit ? t('Edit Report Template') : t('Create Report Template')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
@ -303,11 +290,11 @@ export default class CUD extends Component {
|
|||
<ACEEditor id="js" height="700px" mode="javascript" label={t('Data processing code')} help={<Trans>Write the body of the JavaScript function with signature <code>function(inputs, callback)</code> that returns an object to be rendered by the Handlebars template below.</Trans>}/>
|
||||
<ACEEditor id="hbs" height="700px" mode="handlebars" label={t('Rendering template')} help={<Trans>Use HTML with Handlebars syntax. See documentation <a href="http://handlebarsjs.com/">here</a>.</Trans>}/>
|
||||
|
||||
{edit ?
|
||||
{isEdit ?
|
||||
<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/edit/${this.state.entityId}/delete`}/>
|
||||
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/reports/templates/${this.props.entity.id}/delete`}/>
|
||||
</ButtonRow>
|
||||
:
|
||||
<ButtonRow>
|
||||
|
|
|
@ -50,14 +50,14 @@ export default class List extends Component {
|
|||
if (perms.includes('view')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: '/reports/templates/edit/' + data[0]
|
||||
link: `/reports/templates/${data[0]}/edit`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
|
||||
link: '/reports/templates/share/' + data[0]
|
||||
link: `/reports/templates/${data[0]}/share`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,6 @@ export default class List extends Component {
|
|||
};
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() },
|
||||
|
|
|
@ -21,32 +21,20 @@ export default class Share extends Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
entityId: parseInt(props.match.params.id)
|
||||
};
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.func,
|
||||
getUrl: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
entityTypeId: PropTypes.string
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadEntity() {
|
||||
const response = await axios.get(this.props.getUrl(this.state.entityId));
|
||||
this.setState({
|
||||
entity: response.data
|
||||
});
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async deleteShare(userId) {
|
||||
const data = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.state.entityId,
|
||||
entityId: this.props.entity.id,
|
||||
userId
|
||||
};
|
||||
|
||||
|
@ -58,14 +46,13 @@ export default class Share extends Component {
|
|||
clearShareFields() {
|
||||
this.populateFormValues({
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.state.entityId,
|
||||
entityId: this.props.entity.id,
|
||||
userId: null,
|
||||
role: null
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadEntity();
|
||||
this.clearShareFields();
|
||||
}
|
||||
|
||||
|
@ -151,29 +138,25 @@ export default class Share extends Component {
|
|||
];
|
||||
|
||||
|
||||
if (this.state.entity) {
|
||||
return (
|
||||
<div>
|
||||
<Title>{this.props.title(this.state.entity)}</Title>
|
||||
return (
|
||||
<div>
|
||||
<Title>{this.props.title}</Title>
|
||||
|
||||
<h3 className="legend">{t('Add User')}</h3>
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`/rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.state.entityId}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
|
||||
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={`/rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
|
||||
<h3 className="legend">{t('Add User')}</h3>
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`/rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
|
||||
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={`/rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Share')}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Share')}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
|
||||
<hr/>
|
||||
<h3 className="legend">{t('Existing Users')}</h3>
|
||||
<hr/>
|
||||
<h3 className="legend">{t('Existing Users')}</h3>
|
||||
|
||||
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`/rest/shares-table-by-entity/${this.props.entityTypeId}/${this.state.entityId}`} columns={sharesColumns} actions={actions}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (<p>{t('Loading ...')}</p>)
|
||||
}
|
||||
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`/rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} actions={actions}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
|
@ -17,18 +18,10 @@ export default class UserShares extends Component {
|
|||
super(props);
|
||||
|
||||
this.sharesTables = {};
|
||||
|
||||
this.state = {
|
||||
userId: parseInt(props.match.params.id)
|
||||
};
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadEntity() {
|
||||
const response = await axios.get(`/rest/users/${this.state.userId}`);
|
||||
this.setState({
|
||||
username: response.data.username
|
||||
});
|
||||
static propTypes = {
|
||||
user: PropTypes.object
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
|
@ -36,7 +29,7 @@ export default class UserShares extends Component {
|
|||
const data = {
|
||||
entityTypeId,
|
||||
entityId,
|
||||
userId: this.state.userId
|
||||
userId: this.props.user.id
|
||||
};
|
||||
|
||||
await axios.put('/rest/shares', data);
|
||||
|
@ -46,7 +39,6 @@ export default class UserShares extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadEntity();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -58,7 +50,7 @@ export default class UserShares extends Component {
|
|||
|
||||
if (!autoGenerated && perms.includes('share')) {
|
||||
actions.push({
|
||||
label: 'Delete',
|
||||
label: <span className="glyphicon glyphicon-remove" aria-hidden="true" title="Remove"></span>,
|
||||
action: () => this.deleteShare(entityTypeId, data[2])
|
||||
});
|
||||
}
|
||||
|
@ -74,7 +66,7 @@ export default class UserShares extends Component {
|
|||
return (
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`/rest/shares-table-by-user/${entityTypeId}/${this.state.userId}`} columns={columns} actions={actions}/>
|
||||
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`/rest/shares-table-by-user/${entityTypeId}/${this.props.user.id}`} columns={columns} actions={actions}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -83,7 +75,7 @@ export default class UserShares extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Shares for user "{{username}}"', {username: this.state.username})}</Title>
|
||||
<Title>{t('Shares for user "{{username}}"', {username: this.props.user.username})}</Title>
|
||||
|
||||
{renderSharesTable('namespace', t('Namespaces'))}
|
||||
{renderSharesTable('list', t('Lists'))}
|
||||
|
|
|
@ -5,8 +5,7 @@ import PropTypes from 'prop-types';
|
|||
import { translate } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page';
|
||||
import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button, TableSelect } from '../lib/form';
|
||||
import axios from '../lib/axios';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import { withErrorHandling } from '../lib/error-handling';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import passwordValidator from '../../../shared/password-validator';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
@ -26,10 +25,6 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {};
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.id);
|
||||
}
|
||||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/rest/users-validate',
|
||||
|
@ -40,24 +35,16 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
static propTypes = {
|
||||
edit: PropTypes.bool
|
||||
}
|
||||
|
||||
isDelete() {
|
||||
return this.props.match.params.action === 'delete';
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/users/${this.state.entityId}`, data => {
|
||||
data.password = '';
|
||||
data.password2 = '';
|
||||
});
|
||||
action: PropTypes.string.isRequired,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
data.password = '';
|
||||
data.password2 = '';
|
||||
});
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
username: '',
|
||||
|
@ -72,7 +59,7 @@ export default class CUD extends Component {
|
|||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
||||
const username = state.getIn(['username', 'value']);
|
||||
const usernameServerValidation = state.getIn(['username', 'serverValidation']);
|
||||
|
@ -121,7 +108,7 @@ export default class CUD extends Component {
|
|||
|
||||
let passwordMsgs = [];
|
||||
|
||||
if (!edit && !password) {
|
||||
if (!isEdit && !password) {
|
||||
passwordMsgs.push(t('Password must not be empty'));
|
||||
}
|
||||
|
||||
|
@ -142,12 +129,11 @@ 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/users/${this.state.entityId}`
|
||||
url = `/rest/users/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/users'
|
||||
|
@ -192,30 +178,9 @@ export default class CUD extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
async showDeleteModal() {
|
||||
this.navigateTo(`/users/edit/${this.state.entityId}/delete`);
|
||||
}
|
||||
|
||||
async hideDeleteModal() {
|
||||
this.navigateTo(`/users/edit/${this.state.entityId}`);
|
||||
}
|
||||
|
||||
async performDelete() {
|
||||
const t = this.props.t;
|
||||
|
||||
await this.hideDeleteModal();
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Deleting user...'));
|
||||
|
||||
await axios.delete(`/rest/users/${this.state.entityId}`);
|
||||
|
||||
this.navigateToWithFlashMessage('/users', 'success', t('User deleted'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const isEdit = !!this.props.entity;
|
||||
const userId = this.getFormValue('id');
|
||||
const canDelete = userId !== 1 && mailtrainConfig.userId !== userId;
|
||||
|
||||
|
@ -227,18 +192,18 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{edit && canDelete &&
|
||||
{isEdit && canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/users/${this.state.entityId}`}
|
||||
cudUrl={`/users/edit/${this.state.entityId}`}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/users/${this.props.entity.id}`}
|
||||
cudUrl={`/users/${this.props.entity.id}/edit`}
|
||||
listUrl="/users"
|
||||
deletingMsg={t('Deleting user ...')}
|
||||
deletedMsg={t('User deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit User') : t('Create User')}</Title>
|
||||
<Title>{isEdit ? t('Edit User') : t('Create User')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="username" label={t('User Name')}/>
|
||||
|
@ -255,7 +220,7 @@ export default class CUD extends Component {
|
|||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete User')} linkTo={`/users/edit/${this.state.entityId}/delete`}/>}
|
||||
{isEdit && canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete User')} linkTo={`/users/${this.props.entity.id}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -15,21 +15,23 @@ export default class List extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
// There are no permissions checks here because this page makes no sense for anyone who does not have manageUsers permission
|
||||
// Once someone has this permission, then all on this page can be used.
|
||||
|
||||
const t = this.props.t;
|
||||
|
||||
const actions = data => [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: '/users/edit/' + data[0]
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: `/users/${data[0]}/edit`
|
||||
},
|
||||
{
|
||||
label: 'Shares',
|
||||
link: '/users/shares/' + data[0]
|
||||
label: <span className="glyphicon glyphicon-share" aria-hidden="true" title="Share"></span>,
|
||||
link: `/users/${data[0]}/shares`
|
||||
}
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: "Username" }
|
||||
];
|
||||
|
||||
|
|
|
@ -21,20 +21,29 @@ const getStructure = t => {
|
|||
link: '/users',
|
||||
component: List,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit User'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<CUD edit {...props} />)
|
||||
':userId([0-9]+)': {
|
||||
title: resolved => t('User "{{name}}"', {name: resolved.user.name}),
|
||||
resolve: {
|
||||
user: params => `/rest/users/${params.userId}`
|
||||
},
|
||||
link: params => `/users/${params.userId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
title: t('Edit'),
|
||||
link: params => `/users/${params.userId}/edit`,
|
||||
render: 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} />
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
title: t('Create User'),
|
||||
render: props => (<CUD {...props} />)
|
||||
title: t('Create'),
|
||||
render: props => <CUD action="create" />
|
||||
},
|
||||
shares: {
|
||||
title: t('User Shares'),
|
||||
params: [':id' ],
|
||||
component: UserShares
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ const shares = require('./shares');
|
|||
const fieldsLegacy = require('../lib/models/fields');
|
||||
const bluebird = require('bluebird');
|
||||
const validators = require('../shared/validators');
|
||||
const shortid = require('shortid');
|
||||
|
||||
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
|
||||
const allowedKeysUpdate = new Set(['name', 'key', 'default_value', 'group', 'settings']);
|
||||
|
@ -62,7 +63,7 @@ fieldTypes['radio-enum'] = fieldTypes['dropdown-enum'] = {
|
|||
};
|
||||
|
||||
fieldTypes.option = {
|
||||
validate: entity => [],
|
||||
validate: entity => {},
|
||||
addColumn: (table, name) => table.boolean(name),
|
||||
indexed: true,
|
||||
grouped: false
|
||||
|
@ -156,13 +157,18 @@ async function serverValidate(context, listId, data) {
|
|||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||
|
||||
if (data.key) {
|
||||
const existing = await tx('custom_fields').where({
|
||||
const existingKeyQuery = tx('custom_fields').where({
|
||||
list: listId,
|
||||
key: data.key
|
||||
}).whereNot('id', data.id).first();
|
||||
});
|
||||
|
||||
if (data.id) {
|
||||
existingKeyQuery.whereNot('id', data.id);
|
||||
}
|
||||
|
||||
const existingKey = await existingKeyQuery.first();
|
||||
result.key = {
|
||||
exists: !!existing
|
||||
exists: !!existingKey
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -180,7 +186,6 @@ async function _validateAndPreprocess(tx, listId, entity, isCreate) {
|
|||
enforce(fieldType, 'Unknown field type');
|
||||
|
||||
const validateErrs = fieldType.validate(entity);
|
||||
enforce(!validateErrs.length, 'Invalid field');
|
||||
|
||||
enforce(validators.mergeTagValid(entity.key), 'Merge tag is not valid.');
|
||||
|
||||
|
@ -189,7 +194,7 @@ async function _validateAndPreprocess(tx, listId, entity, isCreate) {
|
|||
key: entity.key
|
||||
});
|
||||
if (!isCreate) {
|
||||
existingWithKeyQuery.whereNot('id', data.id);
|
||||
existingWithKeyQuery.whereNot('id', entity.id);
|
||||
}
|
||||
const existingWithKey = await existingWithKeyQuery.first();
|
||||
if (existingWithKey) {
|
||||
|
@ -255,9 +260,11 @@ async function create(context, listId, entity) {
|
|||
|
||||
await _validateAndPreprocess(tx, listId, entity, true);
|
||||
|
||||
const fieldType = fieldTypes[entity.type];
|
||||
|
||||
let columnName;
|
||||
if (!fieldType.grouped) {
|
||||
columnName = ('custom_' + slugify(name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
|
||||
columnName = ('custom_' + slugify(entity.name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
|
||||
}
|
||||
|
||||
const filteredEntity = filterObject(entity, allowedKeysCreate);
|
||||
|
@ -267,7 +274,7 @@ async function create(context, listId, entity) {
|
|||
const ids = await tx('custom_fields').insert(filteredEntity);
|
||||
id = ids[0];
|
||||
|
||||
_sortIn(tx, listId, id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
|
||||
await _sortIn(tx, listId, id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
|
||||
|
||||
if (columnName) {
|
||||
await knex.schema.table('subscription__' + listId, table => {
|
||||
|
@ -297,10 +304,10 @@ async function updateWithConsistencyCheck(context, listId, entity) {
|
|||
}
|
||||
|
||||
enforce(entity.type === existing.type, 'Field type cannot be changed');
|
||||
await _validateAndPreprocess(tx, listId, entity, true);
|
||||
await _validateAndPreprocess(tx, listId, entity, false);
|
||||
|
||||
await tx('custom_fields').where('id', entity.id).update(filterObject(entity, allowedKeysUpdate));
|
||||
_sortIn(tx, listId, entity.id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
|
||||
await _sortIn(tx, listId, entity.id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ async function getById(context, id) {
|
|||
await knex.transaction(async tx => {
|
||||
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view');
|
||||
entity = await _getById(tx, id);
|
||||
entity.permissions = await shares.getPermissions(tx, context, 'customForm', id);
|
||||
});
|
||||
|
||||
return entity;
|
||||
|
@ -146,7 +147,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
const existing = await _getById(tx, entity.id);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -31,12 +31,19 @@ async function listDTAjax(context, params) {
|
|||
}
|
||||
|
||||
async function getById(context, id) {
|
||||
shares.enforceEntityPermission(context, 'list', id, 'view');
|
||||
let entity;
|
||||
|
||||
const entity = await knex('lists').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
await knex.transaction(async tx => {
|
||||
|
||||
shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view');
|
||||
|
||||
entity = await tx('lists').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
entity.permissions = await shares.getPermissions(tx, context, 'list', id);
|
||||
});
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
@ -73,7 +80,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ const { enforce, filterObject } = require('../lib/helpers');
|
|||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const shares = require('./shares');
|
||||
const permissions = require('../lib/permissions');
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'namespace']);
|
||||
|
||||
|
@ -106,12 +108,19 @@ function hash(entity) {
|
|||
}
|
||||
|
||||
async function getById(context, id) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', id, 'view');
|
||||
let entity;
|
||||
|
||||
const entity = await knex('namespaces').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
await knex.transaction(async tx => {
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', id, 'view');
|
||||
|
||||
entity = await tx('namespaces').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
entity.permissions = await shares.getPermissions(tx, context, 'namespace', id);
|
||||
});
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
@ -145,7 +154,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -15,12 +15,19 @@ function hash(entity) {
|
|||
}
|
||||
|
||||
async function getById(context, id) {
|
||||
await shares.enforceEntityPermission(context, 'reportTemplate', id, 'view');
|
||||
let entity;
|
||||
|
||||
const entity = await knex('report_templates').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
await knex.transaction(async tx => {
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', id, 'view');
|
||||
|
||||
entity = await tx('report_templates').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
entity.permissions = await shares.getPermissions(tx, context, 'reportTemplate', id);
|
||||
});
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
@ -60,7 +67,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,20 +19,27 @@ function hash(entity) {
|
|||
}
|
||||
|
||||
async function getByIdWithTemplate(context, id) {
|
||||
await shares.enforceEntityPermission(context, 'report', id, 'view');
|
||||
let entity;
|
||||
|
||||
const entity = await knex('reports')
|
||||
.where('reports.id', id)
|
||||
.innerJoin('report_templates', 'reports.report_template', 'report_templates.id')
|
||||
.select(['reports.id', 'reports.name', 'reports.description', 'reports.report_template', 'reports.params', 'reports.state', 'reports.namespace', 'report_templates.user_fields', 'report_templates.mime_type', 'report_templates.hbs', 'report_templates.js'])
|
||||
.first();
|
||||
await knex.transaction(async tx => {
|
||||
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'view');
|
||||
|
||||
entity.user_fields = JSON.parse(entity.user_fields);
|
||||
entity.params = JSON.parse(entity.params);
|
||||
entity = await tx('reports')
|
||||
.where('reports.id', id)
|
||||
.innerJoin('report_templates', 'reports.report_template', 'report_templates.id')
|
||||
.select(['reports.id', 'reports.name', 'reports.description', 'reports.report_template', 'reports.params', 'reports.state', 'reports.namespace', 'report_templates.user_fields', 'report_templates.mime_type', 'report_templates.hbs', 'report_templates.js'])
|
||||
.first();
|
||||
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
entity.user_fields = JSON.parse(entity.user_fields);
|
||||
entity.params = JSON.parse(entity.params);
|
||||
|
||||
entity.permissions = await shares.getPermissions(tx, context, 'report', id);
|
||||
});
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
@ -89,7 +96,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
existing.params = JSON.parse(existing.params);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -518,6 +518,17 @@ async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperat
|
|||
}
|
||||
}
|
||||
|
||||
async function getPermissions(tx, context, entityTypeId, entityId) {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
|
||||
const rows = await tx(entityType.permissionsTable)
|
||||
.select('operation')
|
||||
.where('entity', entityId)
|
||||
.where('user', context.user.id);
|
||||
|
||||
return rows.map(x => x.operation);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
listByEntityDTAjax,
|
||||
|
@ -535,5 +546,6 @@ module.exports = {
|
|||
checkTypePermission,
|
||||
enforceGlobalPermission,
|
||||
throwPermissionDenied,
|
||||
regenerateRoleNamesTable
|
||||
regenerateRoleNamesTable,
|
||||
getPermissions
|
||||
};
|
|
@ -9,7 +9,7 @@ const contextHelpers = require('../lib/context-helpers');
|
|||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
router.getAsync('/download/:id', passport.loggedIn, async (req, res) => {
|
||||
router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
|
||||
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
|
||||
|
||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<body class="{{bodyClass}}">
|
||||
|
||||
<nav class="navbar navbar-default navbar-static-top">
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
|
||||
|
@ -155,7 +155,7 @@
|
|||
|
||||
{{#if indexPage}}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<div class="pull-right col-md-4">
|
||||
{{{shoutout}}}
|
||||
</div>
|
||||
|
@ -173,14 +173,14 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
|
||||
{{flash_messages}} {{{body}}}
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<p class="text-muted">© 2016 Kreata OÜ <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{{#translate}}Source on GitHub{{/translate}}</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
Loading…
Reference in a new issue