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
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')
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue