Edit and create seem to more or less work (including selection of the parent). Delete is pending.
This commit is contained in:
parent
61893d77f6
commit
5b82d3b540
12 changed files with 322 additions and 176 deletions
|
@ -8,7 +8,7 @@ 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';
|
||||
import { TreeTable, TreeSelectMode } from './tree';
|
||||
|
||||
const FormState = {
|
||||
Loading: 0,
|
||||
|
@ -16,6 +16,10 @@ const FormState = {
|
|||
Ready: 2
|
||||
};
|
||||
|
||||
const FormSendMethod = {
|
||||
PUT: 0,
|
||||
POST: 1
|
||||
};
|
||||
|
||||
@translate()
|
||||
@withSectionHelpers
|
||||
|
@ -40,26 +44,22 @@ class Form extends Component {
|
|||
async onSubmit(evt) {
|
||||
const t = this.props.t;
|
||||
|
||||
const owner = this.props.stateOwner;
|
||||
|
||||
try {
|
||||
evt.preventDefault();
|
||||
|
||||
if (this.props.onSubmitAsync) {
|
||||
this.props.stateOwner.disableForm();
|
||||
this.props.stateOwner.setFormStatusMessage('info', t('Submitting...'));
|
||||
|
||||
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',
|
||||
owner.disableForm();
|
||||
owner.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
<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>
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ class InputField extends Component {
|
|||
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.bindChangeEventToFormValue(id)}/>
|
||||
<input type="text" value={owner.getFormValue(id)} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class TextArea extends Component {
|
|||
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.bindChangeEventToFormValue(id)}></textarea>
|
||||
<textarea id={htmlId} value={owner.getFormValue(id)} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}></textarea>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -227,7 +227,8 @@ class TreeTableSelect extends Component {
|
|||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
dataUrl: PropTypes.string.isRequired
|
||||
dataUrl: PropTypes.string,
|
||||
data: PropTypes.array
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -251,7 +252,7 @@ class TreeTableSelect extends Component {
|
|||
<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}/>
|
||||
<TreeTable data={this.props.data} dataUrl={this.props.dataUrl} selectMode={TreeSelectMode.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>
|
||||
|
@ -307,7 +308,7 @@ function withForm(target) {
|
|||
this.populateFormValues(data);
|
||||
};
|
||||
|
||||
inst.validateAndPutFormValuesToURL = async function(url, mutator) {
|
||||
inst.validateAndSendFormValuesToURL = async function(method, url, mutator) {
|
||||
if (this.isFormWithoutErrors()) {
|
||||
const data = this.getFormValues();
|
||||
|
||||
|
@ -315,9 +316,15 @@ function withForm(target) {
|
|||
mutator(data);
|
||||
}
|
||||
|
||||
await axios.put(`/namespaces/rest/namespaces/${this.nsId}`, data);
|
||||
if (method === FormSendMethod.PUT) {
|
||||
await axios.put(url, data);
|
||||
} else if (method === FormSendMethod.POST) {
|
||||
await axios.post(url, data);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
this.showFormValidation();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -349,10 +356,6 @@ function withForm(target) {
|
|||
}));
|
||||
};
|
||||
|
||||
inst.bindChangeEventToFormValue = function(name) {
|
||||
return evt => this.updateFormValue(name, evt.target.value);
|
||||
};
|
||||
|
||||
inst.getFormValue = function(name) {
|
||||
return this.state.formState.getIn(['data', name, 'value']);
|
||||
};
|
||||
|
@ -454,5 +457,6 @@ export {
|
|||
TextArea,
|
||||
ButtonRow,
|
||||
Button,
|
||||
TreeTableSelect
|
||||
TreeTableSelect,
|
||||
FormSendMethod
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { withRouter } from 'react-router';
|
|||
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'
|
||||
import './page.css';
|
||||
import { withErrorHandling } from './error-handling';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
|
||||
|
||||
class PageContent extends Component {
|
||||
|
@ -25,9 +26,10 @@ class PageContent extends Component {
|
|||
path = path + '/' + structure.params.join('/');
|
||||
}
|
||||
|
||||
if (structure.component) {
|
||||
if (structure.component || structure.render) {
|
||||
const route = {
|
||||
component: structure.component,
|
||||
render: structure.render,
|
||||
path: (path === '' ? '/' : path)
|
||||
};
|
||||
|
||||
|
@ -43,7 +45,11 @@ class PageContent extends Component {
|
|||
}
|
||||
|
||||
renderRoute(route) {
|
||||
return <Route key={route.path} exact path={route.path} component={route.component} />;
|
||||
if (route.component) {
|
||||
return <Route key={route.path} exact path={route.path} component={route.component} />;
|
||||
} else if (route.render) {
|
||||
return <Route key={route.path} exact path={route.path} render={route.render} />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -182,7 +188,9 @@ class SectionContent extends Component {
|
|||
}
|
||||
|
||||
errorHandler(error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
if (error instanceof interoperableErrors.NotLoggedInError) {
|
||||
this.navigateTo('/users/login?next=' + encodeURIComponent(this.props.root));
|
||||
} else 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);
|
||||
|
@ -282,9 +290,8 @@ class Button extends Component {
|
|||
}
|
||||
|
||||
async onClick(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
if (this.props.onClick) {
|
||||
evt.preventDefault();
|
||||
onClick(evt);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,36 @@
|
|||
|
||||
.mt-treetable-container {
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.mt-treetable-container>table.fancytree-ext-table {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
|
||||
border-top: 0px none;
|
||||
}
|
||||
|
||||
|
||||
.form-group .mt-treetable-container {
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
.form-group.has-success .mt-treetable-container {
|
||||
border-color: #468847;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.form-group.has-error .mt-treetable-container {
|
||||
border-color: #b94a48;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
|
|
@ -10,11 +10,16 @@ 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 axios from './axios';
|
||||
|
||||
import { withSectionHelpers } from '../lib/page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
|
||||
const TreeSelectMode = {
|
||||
NONE: 0,
|
||||
SINGLE: 1,
|
||||
MULTI: 2
|
||||
};
|
||||
|
||||
@translate()
|
||||
@withSectionHelpers
|
||||
|
@ -23,24 +28,25 @@ 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
|
||||
treeData: []
|
||||
};
|
||||
|
||||
this.loadData();
|
||||
if (props.data) {
|
||||
this.state.treeData = props.data;
|
||||
}
|
||||
|
||||
// Select Mode simply cannot be changed later. This is just to make sure we avoid inconsistencies if someone changes it anyway.
|
||||
this.selectMode = this.props.selectMode;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
selectMode: TreeSelectMode.NONE
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadData() {
|
||||
axios.get(this.props.dataUrl)
|
||||
async loadData(dataUrl) {
|
||||
axios.get(dataUrl)
|
||||
.then(response => {
|
||||
this.setState({
|
||||
treeData: [ response.data ]
|
||||
|
@ -49,7 +55,8 @@ class TreeTable extends Component {
|
|||
}
|
||||
|
||||
static propTypes = {
|
||||
dataUrl: PropTypes.string.isRequired,
|
||||
dataUrl: PropTypes.string,
|
||||
data: PropTypes.array,
|
||||
selectMode: PropTypes.number,
|
||||
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||
onSelectionChangedAsync: PropTypes.func,
|
||||
|
@ -57,13 +64,25 @@ class TreeTable extends Component {
|
|||
withHeader: PropTypes.bool
|
||||
}
|
||||
|
||||
static SelectMode = {
|
||||
NONE: 0,
|
||||
SINGLE: 1,
|
||||
MULTI: 2
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.data) {
|
||||
this.setState({
|
||||
treeData: nextProps.data
|
||||
});
|
||||
} else if (nextProps.dataUrl && this.props.dataUrl !== nextProps.dataUrl) {
|
||||
this.loadData(next.props.dataUrl);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.selection !== nextProps.selection || this.state.treeData != nextState.treeData;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.data && this.props.dataUrl) {
|
||||
this.loadData(this.props.dataUrl);
|
||||
}
|
||||
|
||||
const glyphOpts = {
|
||||
map: {
|
||||
expanderClosed: 'glyphicon glyphicon-menu-right',
|
||||
|
@ -100,7 +119,7 @@ class TreeTable extends Component {
|
|||
this.tree = jQuery(this.domTable).fancytree({
|
||||
extensions: ['glyph', 'table'],
|
||||
glyph: glyphOpts,
|
||||
selectMode: (this.selectMode == TreeTable.SelectMode.MULTI ? 2 : 1),
|
||||
selectMode: (this.selectMode === TreeSelectMode.MULTI ? 2 : 1),
|
||||
icon: false,
|
||||
autoScroll: true,
|
||||
scrollParent: jQuery(this.domTableContainer),
|
||||
|
@ -109,9 +128,9 @@ class TreeTable extends Component {
|
|||
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)
|
||||
checkbox: this.selectMode === TreeSelectMode.MULTI,
|
||||
activate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null),
|
||||
select: (this.selectMode === TreeSelectMode.MULTI ? ::this.onSelect : null)
|
||||
}).fancytree("getTree");
|
||||
|
||||
this.updateSelection();
|
||||
|
@ -124,15 +143,15 @@ class TreeTable extends Component {
|
|||
|
||||
updateSelection() {
|
||||
const tree = this.tree;
|
||||
if (this.selectMode == TreeTable.SelectMode.MULTI) {
|
||||
const selectSet = new Set(this.state.selection);
|
||||
if (this.selectMode === TreeSelectMode.MULTI) {
|
||||
const selectSet = new Set(this.props.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);
|
||||
} else if (this.selectMode === TreeSelectMode.SINGLE) {
|
||||
this.tree.activateKey(this.props.selection);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,11 +165,7 @@ class TreeTable extends Component {
|
|||
// Single-select
|
||||
onActivate(event, data) {
|
||||
const selection = this.tree.getActiveNode().key;
|
||||
if (selection !== this.state.selection) {
|
||||
this.setState({
|
||||
selection
|
||||
});
|
||||
|
||||
if (selection !== this.props.selection) {
|
||||
this.onSelectionChanged(selection);
|
||||
}
|
||||
}
|
||||
|
@ -158,10 +173,10 @@ class TreeTable extends Component {
|
|||
// Multi-select
|
||||
onSelect(event, data) {
|
||||
const newSel = this.tree.getSelectedNodes().map(node => node.key).sort();
|
||||
const oldSel = this.state.selection;
|
||||
const oldSel = this.props.selection;
|
||||
|
||||
let updated = false;
|
||||
const length = oldSel.length
|
||||
const length = oldSel.length;
|
||||
if (length === newSel.length) {
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (oldSel[i] !== newSel[i]) {
|
||||
|
@ -174,10 +189,6 @@ class TreeTable extends Component {
|
|||
}
|
||||
|
||||
if (updated) {
|
||||
this.setState({
|
||||
selection: newSel
|
||||
});
|
||||
|
||||
this.onSelectionChanged(selection);
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +200,7 @@ class TreeTable extends Component {
|
|||
const withHeader = props.withHeader;
|
||||
|
||||
let containerClass = 'mt-treetable-container';
|
||||
if (this.selectMode == TreeTable.SelectMode.NONE) {
|
||||
if (this.selectMode === TreeSelectMode.NONE) {
|
||||
containerClass += ' mt-treetable-inactivable';
|
||||
}
|
||||
|
||||
|
@ -197,8 +208,10 @@ class TreeTable extends Component {
|
|||
containerClass += ' mt-treetable-noheader';
|
||||
}
|
||||
|
||||
// FIXME: style={{ height: '100px', overflow: 'auto'}}
|
||||
|
||||
const container =
|
||||
<div className={containerClass} ref={(domElem) => { this.domTableContainer = domElem; }} style={{ height: '100px', overflow: 'auto'}}>
|
||||
<div className={containerClass} ref={(domElem) => { this.domTableContainer = domElem; }} >
|
||||
<table ref={(domElem) => { this.domTable = domElem; }} className="table table-hover table-striped table-condensed">
|
||||
{props.withHeader &&
|
||||
<thead>
|
||||
|
@ -224,5 +237,6 @@ class TreeTable extends Component {
|
|||
}
|
||||
|
||||
export {
|
||||
TreeTable
|
||||
TreeTable,
|
||||
TreeSelectMode
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Title } from "../lib/page";
|
||||
import csfrToken from 'csfrToken';
|
||||
|
||||
@translate()
|
||||
export default class Create extends Component {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
console.log('csfrToken = ' + csfrToken);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Create Namespace')}</Title>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
170
client/src/namespaces/CreateOrEdit.js
Normal file
170
client/src/namespaces/CreateOrEdit.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withSectionHelpers, Title } from '../lib/page'
|
||||
import { withForm, Form, FormSendMethod, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
||||
import axios from '../lib/axios';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withSectionHelpers
|
||||
@withErrorHandling
|
||||
export default class CreateOrEdit extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.initFormState();
|
||||
}
|
||||
|
||||
isEditGlobal() {
|
||||
return this.nsId === 1;
|
||||
}
|
||||
|
||||
removeNsIdSubtree(data) {
|
||||
for (let idx = 0; idx < data.length; idx++) {
|
||||
const entry = data[idx];
|
||||
|
||||
if (entry.key === this.nsId) {
|
||||
data.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.removeNsIdSubtree(entry.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadTreeData() {
|
||||
axios.get("/namespaces/rest/namespacesTree")
|
||||
.then(response => {
|
||||
|
||||
const data = [response.data];
|
||||
|
||||
if (this.props.edit && !this.isEditGlobal()) {
|
||||
this.removeNsIdSubtree(data);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
treeData: data
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const edit = this.props.edit;
|
||||
|
||||
if (edit) {
|
||||
this.nsId = parseInt(this.props.match.params.nsId);
|
||||
this.getFormValuesFromURL(`/namespaces/rest/namespaces/${this.nsId}`, data => {
|
||||
if (data.parent) data.parent = data.parent.toString();
|
||||
});
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
description: '',
|
||||
parent: null
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isEditGlobal()) {
|
||||
this.loadTreeData();
|
||||
}
|
||||
}
|
||||
|
||||
validateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
if (!state.getIn(['name', 'value']).trim()) {
|
||||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
if (!this.isEditGlobal()) {
|
||||
if (!state.getIn(['parent', 'value'])) {
|
||||
state.setIn(['parent', 'error'], t('Parent Namespace must be selected'));
|
||||
} else {
|
||||
state.setIn(['parent', 'error'], null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
let sendMethod, url;
|
||||
if (edit) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/namespaces/rest/namespaces/${this.nsId}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/namespaces/rest/namespaces'
|
||||
}
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving namespace ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
if (data.parent) data.parent = parseInt(data.parent);
|
||||
});
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage('/namespaces', 'success', t('Namespace saved'));
|
||||
} else {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.LoopDetectedError) {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('There has been a loop detected in the assignment of the parent namespace. This is most likely because someone else has changed the parent of some namespace in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteHandler() {
|
||||
this.setFormStatusMessage('Deleting namespace');
|
||||
this.setFormStatusMessage();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{edit ? t('Edit Namespace') : t('Create Namespace')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
|
||||
{!this.isEditGlobal() &&
|
||||
<TreeTableSelect id="parent" label={t('Parent Namespace')} data={this.state.treeData}/>}
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <Button className="btn-danger" icon="remove" label={t('Delete Namespace')}
|
||||
onClickAsync={::this.deleteHandler}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withSectionHelpers } from '../lib/page'
|
||||
import { withForm, Form, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
||||
import { Title } from "../lib/page";
|
||||
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.getFormValuesFromURL(`/namespaces/rest/namespaces/${this.nsId}`, data => {
|
||||
if (data.parent) data.parent = data.parent.toString();
|
||||
});
|
||||
}
|
||||
|
||||
validateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
if (!state.getIn(['name','value']).trim()) {
|
||||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
await this.validateAndPutFormValuesToURL(`/namespaces/rest/namespaces/${this.nsId}`, data => {
|
||||
if (data.parent) data.parent = parseInt(data.parent);
|
||||
});
|
||||
|
||||
this.navigateToWithFlashMessage('/namespaces', 'success', t('Namespace saved'));
|
||||
|
||||
// FIXME - the enable form in form.js gets called. This causes a warning. Check there whether the component is still mounted.
|
||||
}
|
||||
|
||||
async deleteHandler() {
|
||||
this.setFormStatusMessage('Deleting namespace');
|
||||
this.setFormStatusMessage();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Edit Namespace')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,8 +6,7 @@ import { I18nextProvider } from 'react-i18next';
|
|||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page'
|
||||
import Create from './Create'
|
||||
import Edit from './Edit'
|
||||
import CreateOrEdit from './CreateOrEdit'
|
||||
import List from './List'
|
||||
|
||||
const getStructure = t => ({
|
||||
|
@ -23,12 +22,12 @@ const getStructure = t => ({
|
|||
'edit' : {
|
||||
title: t('Edit Namespace'),
|
||||
params: [':nsId'],
|
||||
component: Edit
|
||||
render: props => (<CreateOrEdit edit {...props} />)
|
||||
},
|
||||
'create' : {
|
||||
title: t('Create Namespace'),
|
||||
link: '/namespaces/create',
|
||||
component: Create
|
||||
render: props => (<CreateOrEdit {...props} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -308,7 +308,6 @@ function filterObject(obj, allowedKeys) {
|
|||
}
|
||||
|
||||
function enforce(condition, message) {
|
||||
console.log(condition);
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
|
|
@ -46,6 +46,14 @@ async function updateWithConsistencyCheck(ns) {
|
|||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
let iter = ns;
|
||||
while (iter.parent != null) {
|
||||
iter = await tx('namespaces').where('id', iter.parent).first();
|
||||
if (iter.id == ns.id) {
|
||||
throw new interoperableErrors.LoopDetectedError();
|
||||
}
|
||||
}
|
||||
|
||||
await tx('namespaces').where('id', ns.id).update(filterObject(ns, allowedUpdateKeys));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,13 +17,12 @@ router.all('/rest/*', (req, res, next) => {
|
|||
});
|
||||
|
||||
router.getAsync('/rest/namespaces/:nsId', async (req, res) => {
|
||||
const ns = await namespaces.getById(req.params.nsId);
|
||||
return res.json(ns);
|
||||
const ns = await namespaces.getById(req.params.nsId);
|
||||
return res.json(ns);
|
||||
});
|
||||
|
||||
router.postAsync('/rest/namespaces', passport.csrfProtection, async (req, res) => {
|
||||
console.log(req.body);
|
||||
// await namespaces.create(req.body);
|
||||
await namespaces.create(req.body);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
@ -36,8 +35,7 @@ router.putAsync('/rest/namespaces/:nsId', passport.csrfProtection, async (req, r
|
|||
});
|
||||
|
||||
router.deleteAsync('/rest/namespaces/:nsId', passport.csrfProtection, async (req, res) => {
|
||||
console.log(req.body);
|
||||
// await namespaces.remove(req.params.nsId);
|
||||
await namespaces.remove(req.params.nsId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
@ -58,7 +56,7 @@ router.getAsync('/rest/namespacesTree', async (req, res) => {
|
|||
}
|
||||
|
||||
if (row.parent) {
|
||||
if (entries[row.parent]) {
|
||||
if (!entries[row.parent]) {
|
||||
entries[row.parent] = {
|
||||
children: []
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ class InteroperableError extends Error {
|
|||
|
||||
class NotLoggedInError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotLoggedIn', msg, data);
|
||||
super('NotLoggedInError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,16 +26,29 @@ class NotFoundError extends InteroperableError {
|
|||
}
|
||||
}
|
||||
|
||||
class LoopDetectedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('LoopDetectedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
const errorTypes = {
|
||||
InteroperableError,
|
||||
NotLoggedInError,
|
||||
ChangedError,
|
||||
NotFoundError
|
||||
NotFoundError,
|
||||
LoopDetectedError
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
if (errorObj.type) {
|
||||
return new errorTypes[errorObj.type](errorObj.message, errorObj.data)
|
||||
|
||||
const ctor = errorTypes[errorObj.type];
|
||||
if (ctor) {
|
||||
return new ctor(errorObj.message, errorObj.data);
|
||||
} else {
|
||||
console.log('Warning unknown type of interoperable error: ' + errorObj.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue