Seems that hierarchical error handling works..
TreeTable component seems to work too. Edit is half-way through. Create / delete are TBD.
This commit is contained in:
parent
79ea9e1897
commit
5e4c86f626
24 changed files with 9967 additions and 261 deletions
2
app.js
2
app.js
|
@ -44,7 +44,7 @@ const reports = require('./routes/reports');
|
|||
const reportsTemplates = require('./routes/report-templates');
|
||||
const namespaces = require('./routes/namespaces');
|
||||
|
||||
const interoperableErrors = require('./lib/interoperable-errors');
|
||||
const interoperableErrors = require('./shared/interoperable-errors');
|
||||
|
||||
const app = express();
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"presets": ["stage-1"],
|
||||
"presets": ["es2015", "stage-1"],
|
||||
"plugins": ["transform-react-jsx", "transform-decorators-legacy", "transform-function-bind"]
|
||||
}
|
9226
client/public/fancytree/jquery.fancytree-all.js
Normal file
9226
client/public/fancytree/jquery.fancytree-all.js
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
18
client/src/lib/axios.js
Normal file
18
client/src/lib/axios.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
import csfrToken from 'csfrToken';
|
||||
import axios from 'axios';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
|
||||
const axiosInst = axios.create({
|
||||
headers: {'X-CSRF-TOKEN': csfrToken}
|
||||
});
|
||||
|
||||
const axiosWrapper = {
|
||||
get: (...args) => axiosInst.get(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
|
||||
put: (...args) => axiosInst.put(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
|
||||
post: (...args) => axiosInst.post(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
|
||||
delete: (...args) => axiosInst.delete(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error })
|
||||
};
|
||||
|
||||
export default axiosWrapper;
|
67
client/src/lib/error-handling.js
Normal file
67
client/src/lib/error-handling.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
'use strict';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function withErrorHandling(target) {
|
||||
const inst = target.prototype;
|
||||
|
||||
const contextTypes = target.contextTypes || {};
|
||||
contextTypes.parentErrorHandler = PropTypes.object;
|
||||
target.contextTypes = contextTypes;
|
||||
|
||||
const childContextTypes = target.childContextTypes || {};
|
||||
childContextTypes.parentErrorHandler = PropTypes.object;
|
||||
target.childContextTypes = childContextTypes;
|
||||
|
||||
const existingGetChildContext = inst.getChildContext;
|
||||
if (existingGetChildContext) {
|
||||
inst.getChildContext = function() {
|
||||
const childContext = (this::existingGetChildContext)();
|
||||
childContext.parentErrorHandler = this;
|
||||
return childContext;
|
||||
}
|
||||
} else {
|
||||
inst.getChildContext = function() {
|
||||
return {
|
||||
parentErrorHandler: this
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
function handleError(that, error) {
|
||||
let errorHandled;
|
||||
if (that.errorHandler) {
|
||||
errorHandled = that.errorHandler(error);
|
||||
}
|
||||
|
||||
if (!errorHandled && that.context.parentErrorHandler) {
|
||||
errorHandled = handleError(that.context.parentErrorHandler, error);
|
||||
}
|
||||
|
||||
if (!errorHandled) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function withAsyncErrorHandler(target, name, descriptor) {
|
||||
let fn = descriptor.value;
|
||||
|
||||
descriptor.value = async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(this, error);
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
withErrorHandling,
|
||||
withAsyncErrorHandler
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import axios from 'axios';
|
||||
import axios from './axios';
|
||||
import Immutable from 'immutable';
|
||||
import { translate } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import { withSectionHelpers } from './page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import { TreeTable } from './tree';
|
||||
|
||||
const FormState = {
|
||||
Loading: 0,
|
||||
|
@ -12,7 +16,10 @@ const FormState = {
|
|||
Ready: 2
|
||||
};
|
||||
|
||||
|
||||
@translate()
|
||||
@withSectionHelpers
|
||||
@withErrorHandling
|
||||
class Form extends Component {
|
||||
static propTypes = {
|
||||
stateOwner: PropTypes.object.isRequired,
|
||||
|
@ -20,28 +27,44 @@ class Form extends Component {
|
|||
}
|
||||
|
||||
static childContextTypes = {
|
||||
stateOwner: PropTypes.object
|
||||
formStateOwner: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
stateOwner: this.props.stateOwner
|
||||
formStateOwner: this.props.stateOwner
|
||||
};
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onSubmit(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
const t = this.props.t;
|
||||
|
||||
if (this.props.onSubmitAsync) {
|
||||
this.props.stateOwner.disableForm();
|
||||
this.props.stateOwner.setFormStatusMessage(t('Submitting...'));
|
||||
try {
|
||||
evt.preventDefault();
|
||||
|
||||
await this.props.onSubmitAsync(evt);
|
||||
if (this.props.onSubmitAsync) {
|
||||
this.props.stateOwner.disableForm();
|
||||
this.props.stateOwner.setFormStatusMessage('info', t('Submitting...'));
|
||||
|
||||
this.props.stateOwner.setFormStatusMessage();
|
||||
this.props.stateOwner.enableForm();
|
||||
await this.props.onSubmitAsync(evt);
|
||||
|
||||
this.props.stateOwner.setFormStatusMessage();
|
||||
this.props.stateOwner.enableForm();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.ChangedError) {
|
||||
this.props.stateOwner.disableForm();
|
||||
this.props.stateOwner.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;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,11 +72,12 @@ class Form extends Component {
|
|||
const t = this.props.t;
|
||||
const owner = this.props.stateOwner;
|
||||
const props = this.props;
|
||||
const statusMessage = owner.getFormStatusMessage();
|
||||
const statusMessageText = owner.getFormStatusMessageText();
|
||||
const statusMessageSeverity = owner.getFormStatusMessageSeverity();
|
||||
|
||||
if (!owner.isFormReady()) {
|
||||
if (owner.isFormWithLoadingNotice()) {
|
||||
return <div>{t('Loading ...')}</div>
|
||||
return <p className={`alert alert-info mt-form-status`} role="alert">{t('Loading ...')}</p>
|
||||
} else {
|
||||
return <div></div>;
|
||||
}
|
||||
|
@ -63,7 +87,7 @@ class Form extends Component {
|
|||
<fieldset disabled={owner.isFormDisabled()}>
|
||||
{props.children}
|
||||
</fieldset>
|
||||
{statusMessage && <p className="col-sm-10 col-sm-offset-2 alert alert-info mt-form-status" role="alert">{owner.getFormStatusMessage()}</p>}
|
||||
{statusMessageText && <p className={`col-sm-10 col-sm-offset-2 alert alert-${statusMessageSeverity} mt-form-status`} role="alert">{statusMessageText}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -92,17 +116,17 @@ class InputField extends Component {
|
|||
}
|
||||
|
||||
static contextTypes = {
|
||||
stateOwner: PropTypes.object.isRequired
|
||||
formStateOwner: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const owner = this.context.stateOwner;
|
||||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
|
||||
return wrapInput(id, htmlId, owner, props.label,
|
||||
<input type="text" value={owner.getFormValue(id)} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindToFormValue(id)}/>
|
||||
<input type="text" value={owner.getFormValue(id)} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindChangeEventToFormValue(id)}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -115,17 +139,17 @@ class TextArea extends Component {
|
|||
}
|
||||
|
||||
static contextTypes = {
|
||||
stateOwner: PropTypes.object.isRequired
|
||||
formStateOwner: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const owner = this.context.stateOwner;
|
||||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
|
||||
return wrapInput(id, htmlId, owner, props.label,
|
||||
<textarea id={htmlId} value={owner.getFormValue(id)} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindToFormValue(id)}></textarea>
|
||||
<textarea id={htmlId} value={owner.getFormValue(id)} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindChangeEventToFormValue(id)}></textarea>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -142,6 +166,7 @@ class ButtonRow extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@withErrorHandling
|
||||
class Button extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
|
@ -153,9 +178,10 @@ class Button extends Component {
|
|||
}
|
||||
|
||||
static contextTypes = {
|
||||
stateOwner: PropTypes.object.isRequired
|
||||
formStateOwner: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClick) {
|
||||
evt.preventDefault();
|
||||
|
@ -165,9 +191,9 @@ class Button extends Component {
|
|||
} else if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
|
||||
this.context.stateOwner.disableForm();
|
||||
this.context.formStateOwner.disableForm();
|
||||
await this.props.onClickAsync(evt);
|
||||
this.context.stateOwner.enableForm();
|
||||
this.context.formStateOwner.enableForm();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,23 +223,65 @@ class Button extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
class TreeTableSelect extends Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
dataUrl: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
formStateOwner: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
async onSelectionChangedAsync(sel) {
|
||||
const owner = this.context.formStateOwner;
|
||||
owner.updateFormValue(this.props.id, sel);
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
|
||||
return (
|
||||
<div className={owner.addFormValidationClass('form-group', id)} >
|
||||
<div className="col-sm-2">
|
||||
<label htmlFor={htmlId} className="control-label">{props.label}</label>
|
||||
</div>
|
||||
<div className="col-sm-10">
|
||||
<TreeTable dataUrl={this.props.dataUrl} selectMode={TreeTable.SelectMode.SINGLE} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||
</div>
|
||||
<div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help'}>{owner.getFormValidationMessage(id)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function withForm(target) {
|
||||
const inst = target.prototype;
|
||||
|
||||
const cleanFormState = Immutable.Map({
|
||||
state: FormState.Loading,
|
||||
isValidationShown: false,
|
||||
isDisabled: false,
|
||||
statusMessageText: '',
|
||||
data: Immutable.Map()
|
||||
});
|
||||
|
||||
inst.initFormState = function() {
|
||||
const state = this.state || {};
|
||||
|
||||
state.formState = Immutable.Map({
|
||||
state: FormState.Loading,
|
||||
isValidationShown: false,
|
||||
isDisabled: false,
|
||||
statusMessage: '',
|
||||
data: Immutable.Map()
|
||||
});
|
||||
|
||||
state.formState = cleanFormState;
|
||||
this.state = state;
|
||||
};
|
||||
|
||||
inst.resetFormState = function() {
|
||||
this.setState({
|
||||
formState: cleanFormState
|
||||
});
|
||||
};
|
||||
|
||||
inst.populateFormValuesFromURL = function(url) {
|
||||
setTimeout(() => {
|
||||
this.setState(previousState => {
|
||||
|
@ -226,7 +294,12 @@ function withForm(target) {
|
|||
}, 500);
|
||||
|
||||
axios.get(url).then(response => {
|
||||
this.populateFormValues(response.data);
|
||||
const data = response.data;
|
||||
|
||||
data.originalHash = data.hash;
|
||||
delete data.hash;
|
||||
|
||||
this.populateFormValues(data);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -257,7 +330,7 @@ function withForm(target) {
|
|||
}));
|
||||
};
|
||||
|
||||
inst.bindToFormValue = function(name) {
|
||||
inst.bindChangeEventToFormValue = function(name) {
|
||||
return evt => this.updateFormValue(name, evt.target.value);
|
||||
};
|
||||
|
||||
|
@ -322,12 +395,21 @@ function withForm(target) {
|
|||
return !this.state.formState.get('data').find(attr => attr.get('error'));
|
||||
};
|
||||
|
||||
inst.getFormStatusMessage = function() {
|
||||
return this.state.formState.get('statusMessage');
|
||||
inst.getFormStatusMessageText = function() {
|
||||
return this.state.formState.get('statusMessageText');
|
||||
};
|
||||
|
||||
inst.setFormStatusMessage = function(message) {
|
||||
this.setState(previousState => ({formState: previousState.formState.set('statusMessage', message)}));
|
||||
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.enableForm = function() {
|
||||
|
@ -342,6 +424,7 @@ function withForm(target) {
|
|||
return this.state.formState.get('isDisabled');
|
||||
};
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
|
@ -351,5 +434,6 @@ export {
|
|||
InputField,
|
||||
TextArea,
|
||||
ButtonRow,
|
||||
Button
|
||||
Button,
|
||||
TreeTableSelect
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ i18n
|
|||
ns: ['common'],
|
||||
defaultNS: 'common',
|
||||
|
||||
debug: true,
|
||||
debug: false,
|
||||
|
||||
// cache: {
|
||||
// enabled: true
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.mt-button-row > button{
|
||||
.mt-button-row > button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
|
@ -9,4 +9,12 @@
|
|||
.mt-form-status {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mt-action-links > a {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mt-action-links > a:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,14 @@ import PropTypes from 'prop-types';
|
|||
import { withRouter } from 'react-router';
|
||||
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'
|
||||
import './page.css';
|
||||
import { withErrorHandling } from './error-handling';
|
||||
|
||||
|
||||
class PageContent extends Component {
|
||||
static propTypes = {
|
||||
structure: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
getRoutes(urlPrefix, children) {
|
||||
let routes = [];
|
||||
for (let routeKey in children) {
|
||||
|
@ -49,6 +54,10 @@ class PageContent extends Component {
|
|||
|
||||
@withRouter
|
||||
class Breadcrumb extends Component {
|
||||
static propTypes = {
|
||||
structure: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
renderElement(breadcrumbElem) {
|
||||
if (breadcrumbElem.isActive) {
|
||||
return <li key={breadcrumbElem.idx} className="active">{breadcrumbElem.title}</li>;
|
||||
|
@ -91,6 +100,113 @@ class Breadcrumb extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@translate()
|
||||
class DismissibleAlert extends Component {
|
||||
static propTypes = {
|
||||
severity: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
|
||||
<button type="button" className="close" aria-label={t('Close')} onClick={::this.close}><span aria-hidden="true">×</span></button>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@withRouter
|
||||
@withErrorHandling
|
||||
class SectionContent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
flashMessageText: ''
|
||||
}
|
||||
|
||||
this.historyUnlisten = props.history.listen((location, action) => {
|
||||
this.closeFlashMessage();
|
||||
})
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
structure: PropTypes.object.isRequired,
|
||||
root: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
sectionContent: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
sectionContent: this
|
||||
};
|
||||
}
|
||||
|
||||
getFlashMessageText() {
|
||||
return this.state.flashMessageText;
|
||||
}
|
||||
|
||||
getFlashMessageSeverity() {
|
||||
return this.state.flashMessageSeverity;
|
||||
}
|
||||
|
||||
setFlashMessage(severity, text) {
|
||||
this.setState({
|
||||
flashMessageText: text,
|
||||
flashMessageSeverity: severity
|
||||
});
|
||||
}
|
||||
|
||||
navigateTo(path) {
|
||||
this.props.history.push(path);
|
||||
}
|
||||
|
||||
navigateToWithFlashMessage(path, severity, text) {
|
||||
this.props.history.push(path);
|
||||
this.setFlashMessage(severity, text);
|
||||
}
|
||||
|
||||
errorHandler(error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
|
||||
} else {
|
||||
this.navigateToWithFlashMessage(this.props.root, 'danger', error.message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
closeFlashMessage() {
|
||||
this.setState({
|
||||
flashMessageText: ''
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumb structure={this.props.structure} />
|
||||
{(this.state.flashMessageText && <DismissibleAlert severity={this.state.flashMessageSeverity} onClose={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>)}
|
||||
<PageContent structure={this.props.structure}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@translate()
|
||||
class Section extends Component {
|
||||
constructor(props) {
|
||||
|
@ -104,18 +220,21 @@ class Section extends Component {
|
|||
this.structure = structure;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||
root: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Router>
|
||||
<div>
|
||||
<Breadcrumb structure={this.structure} />
|
||||
<PageContent structure={this.structure}/>
|
||||
</div>
|
||||
<SectionContent root={this.props.root} structure={this.structure} />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Title extends Component {
|
||||
render() {
|
||||
return (
|
||||
|
@ -194,11 +313,43 @@ class Button extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
function withSectionHelpers(target) {
|
||||
const inst = target.prototype;
|
||||
|
||||
const contextTypes = target.contextTypes || {};
|
||||
|
||||
contextTypes.sectionContent = PropTypes.object.isRequired;
|
||||
|
||||
target.contextTypes = contextTypes;
|
||||
|
||||
inst.getFlashMessageText = function() {
|
||||
return this.context.sectionContent.getFlashMessageText();
|
||||
};
|
||||
|
||||
inst.getFlashMessageSeverity = function() {
|
||||
return this.context.sectionContent.getFlashMessageSeverity();
|
||||
};
|
||||
|
||||
inst.setFlashMessage = function(severity, text) {
|
||||
return this.context.sectionContent.setFlashMessage(severity, text);
|
||||
};
|
||||
|
||||
inst.navigateTo = function(path) {
|
||||
return this.context.sectionContent.navigateTo(path);
|
||||
}
|
||||
|
||||
inst.navigateToWithFlashMessage = function(path, severity, text) {
|
||||
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
export {
|
||||
Section,
|
||||
Title,
|
||||
Toolbar,
|
||||
Button,
|
||||
NavButton
|
||||
NavButton,
|
||||
withSectionHelpers
|
||||
};
|
16
client/src/lib/tree.css
Normal file
16
client/src/lib/tree.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
|
||||
border-top: 0px none;
|
||||
}
|
228
client/src/lib/tree.js
Normal file
228
client/src/lib/tree.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { translate } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import jQuery from 'jquery';
|
||||
import '../../public/jquery/jquery-ui-1.12.1.min.js';
|
||||
import '../../public/fancytree/jquery.fancytree-all.js';
|
||||
import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
|
||||
import './tree.css';
|
||||
import axios from 'axios';
|
||||
|
||||
import { withSectionHelpers } from '../lib/page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
|
||||
|
||||
@translate()
|
||||
@withSectionHelpers
|
||||
@withErrorHandling
|
||||
class TreeTable extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.selectMode = this.props.selectMode || TreeTable.SelectMode.NONE;
|
||||
|
||||
let selection = props.selection;
|
||||
if (this.selectMode == TreeTable.SelectMode.MULTI) {
|
||||
selection = selection.slice().sort();
|
||||
}
|
||||
|
||||
this.state = {
|
||||
treeData: [],
|
||||
selection: selection
|
||||
};
|
||||
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadData() {
|
||||
axios.get(this.props.dataUrl)
|
||||
.then(response => {
|
||||
this.setState({
|
||||
treeData: [ response.data ]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dataUrl: PropTypes.string.isRequired,
|
||||
selectMode: PropTypes.number,
|
||||
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||
onSelectionChangedAsync: PropTypes.func,
|
||||
actionLinks: PropTypes.array,
|
||||
withHeader: PropTypes.bool
|
||||
}
|
||||
|
||||
static SelectMode = {
|
||||
NONE: 0,
|
||||
SINGLE: 1,
|
||||
MULTI: 2
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const glyphOpts = {
|
||||
map: {
|
||||
expanderClosed: 'glyphicon glyphicon-menu-right',
|
||||
expanderLazy: 'glyphicon glyphicon-menu-right', // glyphicon-plus-sign
|
||||
expanderOpen: 'glyphicon glyphicon-menu-down', // glyphicon-collapse-down
|
||||
checkbox: 'glyphicon glyphicon-unchecked',
|
||||
checkboxSelected: 'glyphicon glyphicon-check',
|
||||
checkboxUnknown: 'glyphicon glyphicon-share',
|
||||
}
|
||||
};
|
||||
|
||||
let createNodeFn;
|
||||
if (this.props.actionLinks) {
|
||||
const actionLinks = this.props.actionLinks;
|
||||
|
||||
createNodeFn = (event, data) => {
|
||||
const node = data.node;
|
||||
const tdList = jQuery(node.tr).find(">td");
|
||||
|
||||
const linksContainer = jQuery('<span class="mt-action-links"/>');
|
||||
const links = actionLinks.map(({label, link}) => {
|
||||
const lnkHtml = ReactDOMServer.renderToStaticMarkup(<a href="#">{label}</a>);
|
||||
const lnk = jQuery(lnkHtml);
|
||||
lnk.click(() => this.navigateTo(link(node.key)));
|
||||
linksContainer.append(lnk);
|
||||
});
|
||||
|
||||
tdList.eq(1).html(linksContainer);
|
||||
};
|
||||
} else {
|
||||
createNodeFn = (event, data) => {};
|
||||
}
|
||||
|
||||
this.tree = jQuery(this.domTable).fancytree({
|
||||
extensions: ['glyph', 'table'],
|
||||
glyph: glyphOpts,
|
||||
selectMode: (this.selectMode == TreeTable.SelectMode.MULTI ? 2 : 1),
|
||||
icon: false,
|
||||
autoScroll: true,
|
||||
scrollParent: jQuery(this.domTableContainer),
|
||||
source: this.state.treeData,
|
||||
table: {
|
||||
nodeColumnIdx: 0
|
||||
},
|
||||
createNode: createNodeFn,
|
||||
checkbox: this.selectMode == TreeTable.SelectMode.MULTI,
|
||||
activate: (this.selectMode == TreeTable.SelectMode.SINGLE ? ::this.onActivate : null),
|
||||
select: (this.selectMode == TreeTable.SelectMode.MULTI ? ::this.onSelect : null)
|
||||
}).fancytree("getTree");
|
||||
|
||||
this.updateSelection();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.tree.reload(this.state.treeData);
|
||||
this.updateSelection();
|
||||
}
|
||||
|
||||
updateSelection() {
|
||||
const tree = this.tree;
|
||||
if (this.selectMode == TreeTable.SelectMode.MULTI) {
|
||||
const selectSet = new Set(this.state.selection);
|
||||
|
||||
tree.enableUpdate(false);
|
||||
tree.visit(node => node.setSelected(selectSet.has(node.key)));
|
||||
tree.enableUpdate(true);
|
||||
|
||||
} else if (this.selectMode == TreeTable.SelectMode.SINGLE) {
|
||||
this.tree.activateKey(this.state.selection);
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onSelectionChanged(sel) {
|
||||
if (this.props.onSelectionChangedAsync) {
|
||||
await this.props.onSelectionChangedAsync(sel);
|
||||
}
|
||||
}
|
||||
|
||||
// Single-select
|
||||
onActivate(event, data) {
|
||||
const selection = this.tree.getActiveNode().key;
|
||||
if (selection !== this.state.selection) {
|
||||
this.setState({
|
||||
selection
|
||||
});
|
||||
|
||||
this.onSelectionChanged(selection);
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-select
|
||||
onSelect(event, data) {
|
||||
const newSel = this.tree.getSelectedNodes().map(node => node.key).sort();
|
||||
const oldSel = this.state.selection;
|
||||
|
||||
let updated = false;
|
||||
const length = oldSel.length
|
||||
if (length === newSel.length) {
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (oldSel[i] !== newSel[i]) {
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
this.setState({
|
||||
selection: newSel
|
||||
});
|
||||
|
||||
this.onSelectionChanged(selection);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const props = this.props;
|
||||
const actionLinks = props.actionLinks;
|
||||
const withHeader = props.withHeader;
|
||||
|
||||
let containerClass = 'mt-treetable-container';
|
||||
if (this.selectMode == TreeTable.SelectMode.NONE) {
|
||||
containerClass += ' mt-treetable-inactivable';
|
||||
}
|
||||
|
||||
if (!this.withHeader) {
|
||||
containerClass += ' mt-treetable-noheader';
|
||||
}
|
||||
|
||||
const container =
|
||||
<div className={containerClass} ref={(domElem) => { this.domTableContainer = domElem; }} style={{ height: '100px', overflow: 'auto'}}>
|
||||
<table ref={(domElem) => { this.domTable = domElem; }} className="table table-hover table-striped table-condensed">
|
||||
{props.withHeader &&
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('Name')}</th>
|
||||
{actionLinks && <th></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
{actionLinks && <td></td>}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
container
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
TreeTable
|
||||
}
|
|
@ -2,24 +2,25 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import csfrToken from 'csfrToken';
|
||||
import { withForm, Form, InputField, TextArea, ButtonRow, Button} from '../lib/form';
|
||||
import { withSectionHelpers } from '../lib/page'
|
||||
import { withForm, Form, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
||||
import { Title } from "../lib/page";
|
||||
import axios from 'axios';
|
||||
import axios from '../lib/axios';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withSectionHelpers
|
||||
export default class Edit extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.nsId = parseInt(this.props.match.params.nsId);
|
||||
|
||||
console.log('Constructing Edit');
|
||||
this.initFormState();
|
||||
this.populateFormValuesFromURL(`/namespaces/rest/namespaces/${this.nsId}`);
|
||||
}
|
||||
|
||||
|
||||
validateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
|
@ -35,17 +36,15 @@ export default class Edit extends Component {
|
|||
const data = this.getFormValues();
|
||||
console.log(data);
|
||||
|
||||
const response = await axios.put(`/namespaces/rest/namespaces/${this.nsId}`);
|
||||
console.log(response);
|
||||
|
||||
await axios.put(`/namespaces/rest/namespaces/${this.nsId}`, data);
|
||||
} else {
|
||||
this.showFormValidation();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteHandler() {
|
||||
this.setFormStatusMessage('Deleting namespace')
|
||||
this.setFormStatusMessage()
|
||||
this.setFormStatusMessage('Deleting namespace');
|
||||
this.setFormStatusMessage();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -56,13 +55,15 @@ export default class Edit extends Component {
|
|||
<Title>{t('Edit Namespace')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')} description={t('Namespace Name')}/>
|
||||
<TextArea id="description" label={t('Description')} description={t('Description')}/>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Update')}/>
|
||||
<Button className="btn-danger" icon="remove" label={t('Delete Namespace')} onClickAsync={::this.deleteHandler}/>
|
||||
</ButtonRow>
|
||||
|
||||
{this.nsId !== 1 && <TreeTableSelect id="parent" label={t('Parent Namespace')} dataUrl="/namespaces/rest/namespacesTree"/>}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,14 +2,21 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import NamespacesTreeTable from './NamespacesTreeTable';
|
||||
import { Title, Toolbar, NavButton } from "../lib/page";
|
||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
||||
import { TreeTable } from '../lib/tree';
|
||||
|
||||
@translate()
|
||||
export default class List extends Component {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actionLinks = [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: key => '/namespaces/edit/' + key
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
|
@ -18,7 +25,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Namespaces')}</Title>
|
||||
|
||||
<NamespacesTreeTable />
|
||||
<TreeTable withHeader dataUrl="/namespaces/rest/namespacesTree" actionLinks={actionLinks} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
import jQuery from 'jquery';
|
||||
import '../../public/jquery/jquery-ui-1.12.1.min.js';
|
||||
import '../../public/fancytree/jquery.fancytree-all.min.js';
|
||||
import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
|
||||
import axios from 'axios';
|
||||
|
||||
@translate()
|
||||
export default class NamespacesTreeTable extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
treeData: []
|
||||
};
|
||||
|
||||
axios.get('/namespaces/rest/namespacesTree')
|
||||
.then(response => {
|
||||
this.setState({
|
||||
treeData: response.data
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const history = this.props.history;
|
||||
|
||||
const glyphOpts = {
|
||||
map: {
|
||||
expanderClosed: 'glyphicon glyphicon-menu-right',
|
||||
expanderLazy: 'glyphicon glyphicon-menu-right', // glyphicon-plus-sign
|
||||
expanderOpen: 'glyphicon glyphicon-menu-down', // glyphicon-collapse-down
|
||||
}
|
||||
};
|
||||
|
||||
this.tree = jQuery(this.domTable).fancytree({
|
||||
extensions: ['glyph', 'table'],
|
||||
glyph: glyphOpts,
|
||||
selectMode: 1,
|
||||
icon: false,
|
||||
autoScroll: true,
|
||||
scrollParent: jQuery(this.domTableContainer),
|
||||
source: this.state.treeData,
|
||||
table: {
|
||||
nodeColumnIdx: 0
|
||||
},
|
||||
createNode: (event, data) => {
|
||||
const node = data.node;
|
||||
const tdList = jQuery(node.tr).find(">td");
|
||||
tdList.eq(1).html('<a href="#">Edit</a>').click(() => {
|
||||
history.push('/namespaces/edit/' + node.key);
|
||||
});
|
||||
}
|
||||
}).fancytree("getTree");
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.tree.reload(this.state.treeData);
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const container =
|
||||
<div ref={(domElem) => { this.domTableContainer = domElem; }} style={{ height: '100px', overflow: 'auto'}}>
|
||||
<table ref={(domElem) => { this.domTable = domElem; }} className="table table-hover table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('Name')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
container
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -38,7 +38,7 @@ const getStructure = t => ({
|
|||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={ i18n }><Section structure={getStructure}/></I18nextProvider>,
|
||||
<I18nextProvider i18n={ i18n }><Section root='/namespaces' structure={getStructure}/></I18nextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ const path = require('path');
|
|||
|
||||
module.exports = {
|
||||
entry: {
|
||||
namespaces: './src/namespaces/root.js'
|
||||
namespaces: ['babel-polyfill', './src/namespaces/root.js']
|
||||
},
|
||||
output: {
|
||||
library: 'MailtrainReactBody',
|
||||
|
|
|
@ -24,7 +24,8 @@ module.exports = {
|
|||
filterCustomFields,
|
||||
getMjmlTemplate,
|
||||
rollbackAndReleaseConnection,
|
||||
filterObject
|
||||
filterObject,
|
||||
enforce
|
||||
};
|
||||
|
||||
function getDefaultMergeTags(callback) {
|
||||
|
@ -305,3 +306,10 @@ function filterObject(obj, allowedKeys) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
function enforce(condition, message) {
|
||||
console.log(condition);
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
class InteroperableError extends Error {
|
||||
constructor(type, msg, data) {
|
||||
super(msg);
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
class NotLoggedInError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotLoggedIn', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
InteroperableError,
|
||||
NotLoggedInError
|
||||
};
|
|
@ -2,20 +2,8 @@
|
|||
|
||||
const knex = require('../knex');
|
||||
const hasher = require('node-object-hash')();
|
||||
const { filterObject } = require('../helpers');
|
||||
const interoperableErrors = require('../interoperable-errors');
|
||||
|
||||
class ChangedError extends interoperableErrors.InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('namespaces.ChangedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends interoperableErrors.InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('namespaces.NotFoundError', msg, data);
|
||||
}
|
||||
}
|
||||
const { enforce, filterObject } = require('../helpers');
|
||||
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||
|
||||
const allowedKeys = new Set(['id', 'name', 'description', 'parent']);
|
||||
const allowedUpdateKeys = new Set(['name', 'description', 'parent']);
|
||||
|
@ -31,7 +19,7 @@ function hash(ns) {
|
|||
async function getById(nsId) {
|
||||
const ns = await knex('namespaces').where('id', nsId).first();
|
||||
if (!ns) {
|
||||
throw new NotFoundError();
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
ns.hash = hash(ns);
|
||||
|
@ -45,18 +33,20 @@ async function create(ns) {
|
|||
}
|
||||
|
||||
async function updateWithConsistencyCheck(ns) {
|
||||
enforce(ns.id !== 1 || ns.parent === null, 'Cannot assign a parent to the root namespace.');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const existingNs = await tx('namespaces').where('id', ns.id).first();
|
||||
if (!ns) {
|
||||
throw new NotFoundError();
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingNsHash = hash(existingNs);
|
||||
if (existingNsHash != ns.originalHash) {
|
||||
throw new ChangedError();
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
tx('namespaces').where('id', ns.id).update(filterObject(ns, allowedUpdateKeys));
|
||||
await tx('namespaces').where('id', ns.id).update(filterObject(ns, allowedUpdateKeys));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -65,8 +55,6 @@ async function remove(nsId) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
NotFoundError,
|
||||
ChangedError,
|
||||
hash,
|
||||
list,
|
||||
getById,
|
||||
|
|
|
@ -2,32 +2,24 @@
|
|||
|
||||
const express = require('express');
|
||||
|
||||
function safeAsyncHandler(handler) {
|
||||
return function(req, res, next) {
|
||||
handler(req, res).catch(error => next(error));
|
||||
};
|
||||
}
|
||||
|
||||
function replaceLast(elems, replaceFn) {
|
||||
if (elems.length === 0) {
|
||||
return elems;
|
||||
function replaceLastBySafeHandler(handlers) {
|
||||
if (handlers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lastElem = elems[elems.size - 1];
|
||||
const replacement = replaceFn(lastElem);
|
||||
|
||||
elems[elems.size - 1] = replacement;
|
||||
|
||||
return elems;
|
||||
const lastHandler = handlers[handlers.length - 1];
|
||||
const ret = handlers.slice();
|
||||
ret[handlers.length - 1] = (req, res, next) => lastHandler(req, res).catch(error => next(error));
|
||||
return ret;
|
||||
}
|
||||
|
||||
function create() {
|
||||
const router = new express.Router();
|
||||
|
||||
router.getAsync = (path, ...handlers) => router.get(path, ...replaceLast(handlers, safeAsyncHandler));
|
||||
router.postAsync = (path, ...handlers) => router.post(path, ...replaceLast(handlers, safeAsyncHandler));
|
||||
router.putAsync = (path, ...handlers) => router.put(path, ...replaceLast(handlers, safeAsyncHandler));
|
||||
router.deleteAsync = (path, ...handlers) => router.delete(path, ...replaceLast(handlers, safeAsyncHandler));
|
||||
router.getAsync = (path, ...handlers) => router.get(path, ...replaceLastBySafeHandler(handlers));
|
||||
router.postAsync = (path, ...handlers) => router.post(path, ...replaceLastBySafeHandler(handlers));
|
||||
router.putAsync = (path, ...handlers) => router.put(path, ...replaceLastBySafeHandler(handlers));
|
||||
router.deleteAsync = (path, ...handlers) => router.delete(path, ...replaceLastBySafeHandler(handlers));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ const passport = require('../lib/passport');
|
|||
const router = require('../lib/router-async').create();
|
||||
const _ = require('../lib/translate')._;
|
||||
const namespaces = require('../lib/models/namespaces');
|
||||
const interoperableErrors = require('../lib/interoperable-errors');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
router.all('/rest/*', (req, res, next) => {
|
||||
req.needsJSONResponse = true;
|
||||
|
@ -16,7 +16,6 @@ router.all('/rest/*', (req, res, next) => {
|
|||
next();
|
||||
});
|
||||
|
||||
|
||||
router.getAsync('/rest/namespaces/:nsId', async (req, res) => {
|
||||
const ns = await namespaces.getById(req.params.nsId);
|
||||
return res.json(ns);
|
||||
|
@ -29,11 +28,10 @@ router.postAsync('/rest/namespaces', passport.csrfProtection, async (req, res) =
|
|||
});
|
||||
|
||||
router.putAsync('/rest/namespaces/:nsId', passport.csrfProtection, async (req, res) => {
|
||||
console.log(req.body);
|
||||
ns = req.body;
|
||||
ns.id = req.params.nsId;
|
||||
const ns = req.body;
|
||||
ns.id = parseInt(req.params.nsId);
|
||||
|
||||
// await namespaces.updateWithConsistencyCheck(ns);
|
||||
await namespaces.updateWithConsistencyCheck(ns);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
@ -45,24 +43,7 @@ router.deleteAsync('/rest/namespaces/:nsId', passport.csrfProtection, async (req
|
|||
|
||||
router.getAsync('/rest/namespacesTree', async (req, res) => {
|
||||
const entries = {};
|
||||
|
||||
/* Example of roots:
|
||||
[
|
||||
{title: 'A', key: '1', expanded: true},
|
||||
{title: 'B', key: '2', expanded: true, folder: true, children: [
|
||||
{title: 'BA', key: '3', expanded: true, folder: true, children: [
|
||||
{title: 'BAA', key: '4', expanded: true},
|
||||
{title: 'BAB', key: '5', expanded: true}
|
||||
]},
|
||||
{title: 'BB', key: '6', expanded: true, folder: true, children: [
|
||||
{title: 'BBA', key: '7', expanded: true},
|
||||
{title: 'BBB', key: '8', expanded: true}
|
||||
]}
|
||||
]}
|
||||
]
|
||||
*/
|
||||
const roots = [];
|
||||
|
||||
let root; // Only the Root namespace is without a parent
|
||||
const rows = await namespaces.list();
|
||||
|
||||
for (let row of rows) {
|
||||
|
@ -86,14 +67,14 @@ router.getAsync('/rest/namespacesTree', async (req, res) => {
|
|||
entries[row.parent].children.push(entry);
|
||||
|
||||
} else {
|
||||
roots.push(entry);
|
||||
root = entry;
|
||||
}
|
||||
|
||||
entry.title = row.name;
|
||||
entry.key = row.id;
|
||||
}
|
||||
|
||||
return res.json(roots);
|
||||
return res.json(root);
|
||||
});
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
|
@ -101,7 +82,7 @@ router.all('/*', (req, res, next) => {
|
|||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
// res.setSelectedMenu('namespaces');
|
||||
// res.setSelectedMenu('namespaces'); FIXME
|
||||
next();
|
||||
});
|
||||
|
||||
|
|
44
shared/interoperable-errors.js
Normal file
44
shared/interoperable-errors.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
class InteroperableError extends Error {
|
||||
constructor(type, msg, data) {
|
||||
super(msg);
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
class NotLoggedInError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotLoggedIn', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class ChangedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('ChangedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotFoundError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
const errorTypes = {
|
||||
InteroperableError,
|
||||
NotLoggedInError,
|
||||
ChangedError,
|
||||
NotFoundError
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
if (errorObj.type) {
|
||||
return new errorTypes[errorObj.type](errorObj.message, errorObj.data)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Object.assign({}, errorTypes, {
|
||||
deserialize
|
||||
});
|
Loading…
Reference in a new issue