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:
Tomas Bures 2017-06-05 23:59:08 +02:00
parent 79ea9e1897
commit 5e4c86f626
24 changed files with 9967 additions and 261 deletions

View file

@ -1,4 +1,4 @@
{
"presets": ["stage-1"],
"presets": ["es2015", "stage-1"],
"plugins": ["transform-react-jsx", "transform-decorators-legacy", "transform-function-bind"]
}

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
View 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;

View 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
}

View file

@ -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
}

View file

@ -14,7 +14,7 @@ i18n
ns: ['common'],
defaultNS: 'common',
debug: true,
debug: false,
// cache: {
// enabled: true

View file

@ -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;
}

View file

@ -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">&times;</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
View 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
View 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
}

View file

@ -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>
);

View file

@ -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>
);
}

View file

@ -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
);
}
}

View file

@ -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')
);
};

View file

@ -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',