Release candidate of namespace CRUD
This commit is contained in:
parent
5b82d3b540
commit
8e54879539
11 changed files with 354 additions and 111 deletions
|
@ -5,6 +5,7 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
|
"watch": "webpack --watch --watch-poll",
|
||||||
"locales": "for moFile in ../languages/*.mo; do lng=`basename $moFile .mo`; i18next-conv -s $moFile -t locales/$lng/common.json -l $lng; done"
|
"locales": "for moFile in ../languages/*.mo; do lng=`basename $moFile .mo`; i18next-conv -s $moFile -t locales/$lng/common.json -l $lng; done"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
173
client/src/lib/bootstrap-components.js
vendored
Normal file
173
client/src/lib/bootstrap-components.js
vendored
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { translate } from 'react-i18next';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withErrorHandling
|
||||||
|
class DismissibleAlert extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
severity: PropTypes.string.isRequired,
|
||||||
|
onCloseAsync: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
@withAsyncErrorHandler
|
||||||
|
onClose() {
|
||||||
|
if (this.props.onCloseAsync) {
|
||||||
|
this.props.onCloseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.onClose}><span aria-hidden="true">×</span></button>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@withErrorHandling
|
||||||
|
class Button extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
onClickAsync: PropTypes.func,
|
||||||
|
label: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
className: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
@withAsyncErrorHandler
|
||||||
|
async onClick(evt) {
|
||||||
|
if (this.props.onClickAsync) {
|
||||||
|
evt.preventDefault();
|
||||||
|
await this.props.onClickAsync(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
|
||||||
|
let className = 'btn';
|
||||||
|
if (props.className) {
|
||||||
|
className = className + ' ' + props.className;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
if (props.icon) {
|
||||||
|
icon = <span className={'glyphicon glyphicon-' + props.icon}></span>
|
||||||
|
}
|
||||||
|
|
||||||
|
let iconSpacer;
|
||||||
|
if (props.icon && props.label) {
|
||||||
|
iconSpacer = ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" className={className} onClick={::this.onClick}>{icon}{iconSpacer}{props.label}</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withErrorHandling
|
||||||
|
class ModalDialog extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const t = props.t;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
buttons: this.props.buttons || [ { label: t('Close'), className: 'btn-default', onClickAsync: null } ]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.buttonClicked = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
onCloseAsync: PropTypes.func,
|
||||||
|
onButtonClickAsync: PropTypes.func,
|
||||||
|
buttons: PropTypes.array
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const jqModal = jQuery(this.domModal);
|
||||||
|
|
||||||
|
jqModal.on('shown.bs.modal', () => jqModal.focus());
|
||||||
|
jqModal.on('hidden.bs.modal', () => this.onHide());
|
||||||
|
jqModal.modal();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
const jqModal = jQuery(this.domModal);
|
||||||
|
jqModal.modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
@withAsyncErrorHandler
|
||||||
|
async onHide() {
|
||||||
|
if (this.buttonClicked === null) {
|
||||||
|
if (this.props.onCloseAsync) {
|
||||||
|
await this.props.onCloseAsync();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const idx = this.buttonClicked;
|
||||||
|
this.buttonClicked = null;
|
||||||
|
|
||||||
|
const buttonSpec = this.state.buttons[idx];
|
||||||
|
if (buttonSpec.onClickAsync) {
|
||||||
|
await buttonSpec.onClickAsync(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onButtonClick(idx) {
|
||||||
|
this.buttonClicked = idx;
|
||||||
|
const jqModal = jQuery(this.domModal);
|
||||||
|
jqModal.modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
const t = props.t;
|
||||||
|
|
||||||
|
const buttons = [];
|
||||||
|
for (let idx = 0; idx < this.state.buttons.length; idx++) {
|
||||||
|
const buttonSpec = this.state.buttons[idx];
|
||||||
|
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={() => this.onButtonClick(idx)} />
|
||||||
|
buttons.push(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={(domElem) => { this.domModal = domElem; }} className="modal fade" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||||
|
<div className="modal-dialog" role="document">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<button type="button" className="close" aria-label={t('Close')} onClick={::this.close}><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 className="modal-title">{this.props.title}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">{this.props.children}</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
Button,
|
||||||
|
DismissibleAlert,
|
||||||
|
ModalDialog
|
||||||
|
};
|
|
@ -2,9 +2,30 @@
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorHandled;
|
||||||
|
}
|
||||||
|
|
||||||
function withErrorHandling(target) {
|
function withErrorHandling(target) {
|
||||||
const inst = target.prototype;
|
const inst = target.prototype;
|
||||||
|
|
||||||
|
if (inst._withErrorHandlingApplied) return;
|
||||||
|
inst._withErrorHandlingApplied = true;
|
||||||
|
|
||||||
const contextTypes = target.contextTypes || {};
|
const contextTypes = target.contextTypes || {};
|
||||||
contextTypes.parentErrorHandler = PropTypes.object;
|
contextTypes.parentErrorHandler = PropTypes.object;
|
||||||
target.contextTypes = contextTypes;
|
target.contextTypes = contextTypes;
|
||||||
|
@ -28,24 +49,23 @@ function withErrorHandling(target) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Example of use:
|
||||||
|
this.getFormValuesFromURL(....).catch(error => this.handleError(error));
|
||||||
|
|
||||||
|
It's equivalent to:
|
||||||
|
|
||||||
|
@withAsyncErrorHandler
|
||||||
|
async loadFormValues() {
|
||||||
|
await this.getFormValuesFromURL(...);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
inst.handleError = function(error) {
|
||||||
|
handleError(this, error);
|
||||||
|
};
|
||||||
|
|
||||||
return target;
|
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) {
|
function withAsyncErrorHandler(target, name, descriptor) {
|
||||||
let fn = descriptor.value;
|
let fn = descriptor.value;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Immutable from 'immutable';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||||
import { withSectionHelpers } from './page'
|
import { withPageHelpers } from './page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
import { TreeTable, TreeSelectMode } from './tree';
|
import { TreeTable, TreeSelectMode } from './tree';
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ const FormSendMethod = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withSectionHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
class Form extends Component {
|
class Form extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'
|
||||||
import './page.css';
|
import './page.css';
|
||||||
import { withErrorHandling } from './error-handling';
|
import { withErrorHandling } from './error-handling';
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||||
|
import { DismissibleAlert, Button } from './bootstrap-components';
|
||||||
|
|
||||||
|
|
||||||
class PageContent extends Component {
|
class PageContent extends Component {
|
||||||
|
@ -106,31 +107,6 @@ 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
|
@withRouter
|
||||||
|
@ -146,6 +122,30 @@ class SectionContent extends Component {
|
||||||
this.historyUnlisten = props.history.listen((location, action) => {
|
this.historyUnlisten = props.history.listen((location, action) => {
|
||||||
this.closeFlashMessage();
|
this.closeFlashMessage();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------------
|
||||||
|
/* FIXME - remove this once we migrate fully to React
|
||||||
|
This part transforms the flash notice rendered by the server to flash notice managed by React client.
|
||||||
|
It is used primarily for the login info, but there may be some other cases.
|
||||||
|
*/
|
||||||
|
const alrt = jQuery('.container>.alert');
|
||||||
|
alrt.find('button').remove();
|
||||||
|
|
||||||
|
const alrtText = alrt.text();
|
||||||
|
if (alrtText) {
|
||||||
|
this.state.flashMessageText = alrtText;
|
||||||
|
|
||||||
|
const severityRegex = /alert-([^ ]*)/;
|
||||||
|
const match = alrt.attr('class').match(severityRegex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
this.state.flashMessageSeverity = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alrt.remove();
|
||||||
|
// -------------------------------------------------------------------------------------------------------
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -189,7 +189,7 @@ class SectionContent extends Component {
|
||||||
|
|
||||||
errorHandler(error) {
|
errorHandler(error) {
|
||||||
if (error instanceof interoperableErrors.NotLoggedInError) {
|
if (error instanceof interoperableErrors.NotLoggedInError) {
|
||||||
this.navigateTo('/users/login?next=' + encodeURIComponent(this.props.root));
|
window.location = '/users/login?next=' + encodeURIComponent(this.props.root);
|
||||||
} else if (error.response && error.response.data && error.response.data.message) {
|
} else if (error.response && error.response.data && error.response.data.message) {
|
||||||
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
|
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
|
||||||
} else {
|
} else {
|
||||||
|
@ -198,7 +198,7 @@ class SectionContent extends Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeFlashMessage() {
|
async closeFlashMessage() {
|
||||||
this.setState({
|
this.setState({
|
||||||
flashMessageText: ''
|
flashMessageText: ''
|
||||||
})
|
})
|
||||||
|
@ -208,7 +208,7 @@ class SectionContent extends Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb structure={this.props.structure} />
|
<Breadcrumb structure={this.props.structure} />
|
||||||
{(this.state.flashMessageText && <DismissibleAlert severity={this.state.flashMessageSeverity} onClose={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>)}
|
{(this.state.flashMessageText && <DismissibleAlert severity={this.state.flashMessageSeverity} onCloseAsync={::this.closeFlashMessage}>{this.state.flashMessageText}</DismissibleAlert>)}
|
||||||
<PageContent structure={this.props.structure}/>
|
<PageContent structure={this.props.structure}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -281,46 +281,10 @@ class NavButton extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Button extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
label: PropTypes.string,
|
|
||||||
icon: PropTypes.string,
|
|
||||||
className: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClick(evt) {
|
function withPageHelpers(target) {
|
||||||
if (this.props.onClick) {
|
withErrorHandling(target);
|
||||||
evt.preventDefault();
|
|
||||||
onClick(evt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const props = this.props;
|
|
||||||
|
|
||||||
let className = 'btn';
|
|
||||||
if (props.className) {
|
|
||||||
className = className + ' ' + props.className;
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon;
|
|
||||||
if (props.icon) {
|
|
||||||
icon = <span className={'glyphicon glyphicon-' + props.icon}></span>
|
|
||||||
}
|
|
||||||
|
|
||||||
let iconSpacer;
|
|
||||||
if (props.icon && props.label) {
|
|
||||||
iconSpacer = ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button type="button" className={className} onClick={::this.onClick}>{icon}{iconSpacer}{props.label}</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function withSectionHelpers(target) {
|
|
||||||
const inst = target.prototype;
|
const inst = target.prototype;
|
||||||
|
|
||||||
const contextTypes = target.contextTypes || {};
|
const contextTypes = target.contextTypes || {};
|
||||||
|
@ -349,6 +313,8 @@ function withSectionHelpers(target) {
|
||||||
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inst.axios
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,7 +322,6 @@ export {
|
||||||
Section,
|
Section,
|
||||||
Title,
|
Title,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Button,
|
|
||||||
NavButton,
|
NavButton,
|
||||||
withSectionHelpers
|
withPageHelpers
|
||||||
};
|
};
|
|
@ -7,6 +7,10 @@
|
||||||
color: #333333;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-treetable-container {
|
.mt-treetable-container {
|
||||||
padding-top: 9px;
|
padding-top: 9px;
|
||||||
padding-bottom: 9px;
|
padding-bottom: 9px;
|
||||||
|
|
|
@ -12,7 +12,7 @@ import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
|
||||||
import './tree.css';
|
import './tree.css';
|
||||||
import axios from './axios';
|
import axios from './axios';
|
||||||
|
|
||||||
import { withSectionHelpers } from '../lib/page'
|
import { withPageHelpers } from '../lib/page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
|
|
||||||
const TreeSelectMode = {
|
const TreeSelectMode = {
|
||||||
|
@ -22,7 +22,7 @@ const TreeSelectMode = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withSectionHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
class TreeTable extends Component {
|
class TreeTable extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -46,12 +46,17 @@ class TreeTable extends Component {
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async loadData(dataUrl) {
|
async loadData(dataUrl) {
|
||||||
axios.get(dataUrl)
|
const response = await axios.get(dataUrl);
|
||||||
.then(response => {
|
const treeData = response.data;
|
||||||
|
|
||||||
|
treeData.expanded = true;
|
||||||
|
for (const child of treeData.children) {
|
||||||
|
child.expanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
treeData: [ response.data ]
|
treeData: [ response.data ]
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
|
@ -2,21 +2,23 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { withSectionHelpers, Title } from '../lib/page'
|
import { withPageHelpers, Title } from '../lib/page'
|
||||||
import { withForm, Form, FormSendMethod, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
import { withForm, Form, FormSendMethod, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||||
|
import { ModalDialog } from "../lib/bootstrap-components";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
@withSectionHelpers
|
@withPageHelpers
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
export default class CreateOrEdit extends Component {
|
export default class CUD extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.initFormState();
|
this.initFormState();
|
||||||
|
this.hasChildren = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEditGlobal() {
|
isEditGlobal() {
|
||||||
|
@ -28,6 +30,10 @@ export default class CreateOrEdit extends Component {
|
||||||
const entry = data[idx];
|
const entry = data[idx];
|
||||||
|
|
||||||
if (entry.key === this.nsId) {
|
if (entry.key === this.nsId) {
|
||||||
|
if (entry.children.length > 0) {
|
||||||
|
this.hasChildren = true;
|
||||||
|
}
|
||||||
|
|
||||||
data.splice(idx, 1);
|
data.splice(idx, 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +49,7 @@ export default class CreateOrEdit extends Component {
|
||||||
axios.get("/namespaces/rest/namespacesTree")
|
axios.get("/namespaces/rest/namespacesTree")
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
|
||||||
|
response.data.expanded = true;
|
||||||
const data = [response.data];
|
const data = [response.data];
|
||||||
|
|
||||||
if (this.props.edit && !this.isEditGlobal()) {
|
if (this.props.edit && !this.isEditGlobal()) {
|
||||||
|
@ -55,14 +62,19 @@ export default class CreateOrEdit extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@withAsyncErrorHandler
|
||||||
|
async loadFormValues() {
|
||||||
|
await this.getFormValuesFromURL(`/namespaces/rest/namespaces/${this.nsId}`, data => {
|
||||||
|
if (data.parent) data.parent = data.parent.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const edit = this.props.edit;
|
const edit = this.props.edit;
|
||||||
|
|
||||||
if (edit) {
|
if (edit) {
|
||||||
this.nsId = parseInt(this.props.match.params.nsId);
|
this.nsId = parseInt(this.props.match.params.nsId);
|
||||||
this.getFormValuesFromURL(`/namespaces/rest/namespaces/${this.nsId}`, data => {
|
this.loadFormValues();
|
||||||
if (data.parent) data.parent = data.parent.toString();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.populateFormValues({
|
this.populateFormValues({
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -138,17 +150,63 @@ export default class CreateOrEdit extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteHandler() {
|
async showDeleteModal() {
|
||||||
this.setFormStatusMessage('Deleting namespace');
|
this.setState({
|
||||||
this.setFormStatusMessage();
|
deleteConfirmationShown: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async hideDeleteModal() {
|
||||||
|
this.setState({
|
||||||
|
deleteConfirmationShown: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async performDelete() {
|
||||||
|
await this.hideDeleteModal();
|
||||||
|
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.disableForm();
|
||||||
|
this.setFormStatusMessage('info', t('Deleting namespace...'));
|
||||||
|
|
||||||
|
await axios.delete(`/namespaces/rest/namespaces/${this.nsId}`);
|
||||||
|
|
||||||
|
this.navigateToWithFlashMessage('/namespaces', 'success', t('Namespace deleted'));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof interoperableErrors.ChildDetectedError) {
|
||||||
|
this.disableForm();
|
||||||
|
this.setFormStatusMessage('danger',
|
||||||
|
<span>
|
||||||
|
<strong>{t('The namespace cannot be deleted.')}</strong>{' '}
|
||||||
|
{t('There has been a child namespace found. 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.')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const edit = this.props.edit;
|
const edit = this.props.edit;
|
||||||
|
const deleteConfirmationShown = this.state.deleteConfirmationShown;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{!this.isEditGlobal() && deleteConfirmationShown &&
|
||||||
|
<ModalDialog title={t('Confirm deletion')} onCloseAsync={::this.hideDeleteModal} buttons={[
|
||||||
|
{ label: t('No'), className: 'btn-primary', onClickAsync: ::this.hideDeleteModal },
|
||||||
|
{ label: t('Yes'), className: 'btn-danger', onClickAsync: ::this.performDelete }
|
||||||
|
]}>
|
||||||
|
{t('Are you sure you want to delete namespace "{{namespace}}"?', {namespace: this.getFormValue('name')})}
|
||||||
|
</ModalDialog>
|
||||||
|
}
|
||||||
|
|
||||||
<Title>{edit ? t('Edit Namespace') : t('Create Namespace')}</Title>
|
<Title>{edit ? t('Edit Namespace') : t('Create Namespace')}</Title>
|
||||||
|
|
||||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||||
|
@ -160,8 +218,8 @@ export default class CreateOrEdit extends Component {
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||||
{edit && <Button className="btn-danger" icon="remove" label={t('Delete Namespace')}
|
{!this.isEditGlobal() && !this.hasChildren && edit && <Button className="btn-danger" icon="remove" label={t('Delete Namespace')}
|
||||||
onClickAsync={::this.deleteHandler}/>}
|
onClickAsync={::this.showDeleteModal}/>}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
|
@ -6,7 +6,7 @@ import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../lib/i18n';
|
import i18n from '../lib/i18n';
|
||||||
|
|
||||||
import { Section } from '../lib/page'
|
import { Section } from '../lib/page'
|
||||||
import CreateOrEdit from './CreateOrEdit'
|
import CUD from './CUD'
|
||||||
import List from './List'
|
import List from './List'
|
||||||
|
|
||||||
const getStructure = t => ({
|
const getStructure = t => ({
|
||||||
|
@ -22,12 +22,12 @@ const getStructure = t => ({
|
||||||
'edit' : {
|
'edit' : {
|
||||||
title: t('Edit Namespace'),
|
title: t('Edit Namespace'),
|
||||||
params: [':nsId'],
|
params: [':nsId'],
|
||||||
render: props => (<CreateOrEdit edit {...props} />)
|
render: props => (<CUD edit {...props} />)
|
||||||
},
|
},
|
||||||
'create' : {
|
'create' : {
|
||||||
title: t('Create Namespace'),
|
title: t('Create Namespace'),
|
||||||
link: '/namespaces/create',
|
link: '/namespaces/create',
|
||||||
render: props => (<CreateOrEdit {...props} />)
|
render: props => (<CUD {...props} />)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,16 @@ async function updateWithConsistencyCheck(ns) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(nsId) {
|
async function remove(nsId) {
|
||||||
await knex('namespaces').where('id', nsId).del();
|
enforce(nsId !== 1, 'Cannot delete the root namespace.');
|
||||||
|
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
const childNs = await tx('namespaces').where('parent', nsId).first();
|
||||||
|
if (childNs) {
|
||||||
|
throw new interoperableErrors.ChildDetectedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx('namespaces').where('id', nsId).del();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -32,12 +32,20 @@ class LoopDetectedError extends InteroperableError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChildDetectedError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('ChildDetectedError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const errorTypes = {
|
const errorTypes = {
|
||||||
InteroperableError,
|
InteroperableError,
|
||||||
NotLoggedInError,
|
NotLoggedInError,
|
||||||
ChangedError,
|
ChangedError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
LoopDetectedError
|
LoopDetectedError,
|
||||||
|
ChildDetectedError
|
||||||
};
|
};
|
||||||
|
|
||||||
function deserialize(errorObj) {
|
function deserialize(errorObj) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue