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",
|
||||
"scripts": {
|
||||
"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"
|
||||
},
|
||||
"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';
|
||||
|
||||
|
||||
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) {
|
||||
const inst = target.prototype;
|
||||
|
||||
if (inst._withErrorHandlingApplied) return;
|
||||
inst._withErrorHandlingApplied = true;
|
||||
|
||||
const contextTypes = target.contextTypes || {};
|
||||
contextTypes.parentErrorHandler = PropTypes.object;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ 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 { withPageHelpers } from './page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import { TreeTable, TreeSelectMode } from './tree';
|
||||
|
||||
|
@ -22,7 +22,7 @@ const FormSendMethod = {
|
|||
};
|
||||
|
||||
@translate()
|
||||
@withSectionHelpers
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
class Form extends Component {
|
||||
static propTypes = {
|
||||
|
@ -57,9 +57,9 @@ class Form extends Component {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ 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';
|
||||
import { DismissibleAlert, Button } from './bootstrap-components';
|
||||
|
||||
|
||||
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
|
||||
|
@ -146,6 +122,30 @@ class SectionContent extends Component {
|
|||
this.historyUnlisten = props.history.listen((location, action) => {
|
||||
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 = {
|
||||
|
@ -189,7 +189,7 @@ class SectionContent extends Component {
|
|||
|
||||
errorHandler(error) {
|
||||
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) {
|
||||
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
|
||||
} else {
|
||||
|
@ -198,7 +198,7 @@ class SectionContent extends Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
closeFlashMessage() {
|
||||
async closeFlashMessage() {
|
||||
this.setState({
|
||||
flashMessageText: ''
|
||||
})
|
||||
|
@ -208,7 +208,7 @@ class SectionContent extends Component {
|
|||
return (
|
||||
<div>
|
||||
<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}/>
|
||||
</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) {
|
||||
if (this.props.onClick) {
|
||||
evt.preventDefault();
|
||||
onClick(evt);
|
||||
}
|
||||
}
|
||||
function withPageHelpers(target) {
|
||||
withErrorHandling(target);
|
||||
|
||||
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 contextTypes = target.contextTypes || {};
|
||||
|
@ -349,6 +313,8 @@ function withSectionHelpers(target) {
|
|||
return this.context.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||
}
|
||||
|
||||
inst.axios
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
|
@ -356,7 +322,6 @@ export {
|
|||
Section,
|
||||
Title,
|
||||
Toolbar,
|
||||
Button,
|
||||
NavButton,
|
||||
withSectionHelpers
|
||||
withPageHelpers
|
||||
};
|
|
@ -7,6 +7,10 @@
|
|||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mt-treetable-container {
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
|
|
|
@ -12,7 +12,7 @@ import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
|
|||
import './tree.css';
|
||||
import axios from './axios';
|
||||
|
||||
import { withSectionHelpers } from '../lib/page'
|
||||
import { withPageHelpers } from '../lib/page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
|
||||
const TreeSelectMode = {
|
||||
|
@ -22,7 +22,7 @@ const TreeSelectMode = {
|
|||
};
|
||||
|
||||
@translate()
|
||||
@withSectionHelpers
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
class TreeTable extends Component {
|
||||
constructor(props) {
|
||||
|
@ -46,12 +46,17 @@ class TreeTable extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadData(dataUrl) {
|
||||
axios.get(dataUrl)
|
||||
.then(response => {
|
||||
this.setState({
|
||||
treeData: [ response.data ]
|
||||
});
|
||||
});
|
||||
const response = await axios.get(dataUrl);
|
||||
const treeData = response.data;
|
||||
|
||||
treeData.expanded = true;
|
||||
for (const child of treeData.children) {
|
||||
child.expanded = true;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
treeData: [ response.data ]
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
|
|
|
@ -2,21 +2,23 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
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 axios from '../lib/axios';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import { ModalDialog } from "../lib/bootstrap-components";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withSectionHelpers
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class CreateOrEdit extends Component {
|
||||
export default class CUD extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.initFormState();
|
||||
this.hasChildren = false;
|
||||
}
|
||||
|
||||
isEditGlobal() {
|
||||
|
@ -28,6 +30,10 @@ export default class CreateOrEdit extends Component {
|
|||
const entry = data[idx];
|
||||
|
||||
if (entry.key === this.nsId) {
|
||||
if (entry.children.length > 0) {
|
||||
this.hasChildren = true;
|
||||
}
|
||||
|
||||
data.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
|
@ -43,6 +49,7 @@ export default class CreateOrEdit extends Component {
|
|||
axios.get("/namespaces/rest/namespacesTree")
|
||||
.then(response => {
|
||||
|
||||
response.data.expanded = true;
|
||||
const data = [response.data];
|
||||
|
||||
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() {
|
||||
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();
|
||||
});
|
||||
this.loadFormValues();
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
|
@ -138,17 +150,63 @@ export default class CreateOrEdit extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
async deleteHandler() {
|
||||
this.setFormStatusMessage('Deleting namespace');
|
||||
this.setFormStatusMessage();
|
||||
async showDeleteModal() {
|
||||
this.setState({
|
||||
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() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const deleteConfirmationShown = this.state.deleteConfirmationShown;
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
|
@ -160,8 +218,8 @@ export default class CreateOrEdit extends Component {
|
|||
|
||||
<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}/>}
|
||||
{!this.isEditGlobal() && !this.hasChildren && edit && <Button className="btn-danger" icon="remove" label={t('Delete Namespace')}
|
||||
onClickAsync={::this.showDeleteModal}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
|
@ -6,7 +6,7 @@ import { I18nextProvider } from 'react-i18next';
|
|||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page'
|
||||
import CreateOrEdit from './CreateOrEdit'
|
||||
import CUD from './CUD'
|
||||
import List from './List'
|
||||
|
||||
const getStructure = t => ({
|
||||
|
@ -22,12 +22,12 @@ const getStructure = t => ({
|
|||
'edit' : {
|
||||
title: t('Edit Namespace'),
|
||||
params: [':nsId'],
|
||||
render: props => (<CreateOrEdit edit {...props} />)
|
||||
render: props => (<CUD edit {...props} />)
|
||||
},
|
||||
'create' : {
|
||||
title: t('Create Namespace'),
|
||||
link: '/namespaces/create',
|
||||
render: props => (<CreateOrEdit {...props} />)
|
||||
render: props => (<CUD {...props} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,16 @@ async function updateWithConsistencyCheck(ns) {
|
|||
}
|
||||
|
||||
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 = {
|
||||
|
|
|
@ -32,12 +32,20 @@ class LoopDetectedError extends InteroperableError {
|
|||
}
|
||||
}
|
||||
|
||||
class ChildDetectedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('ChildDetectedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const errorTypes = {
|
||||
InteroperableError,
|
||||
NotLoggedInError,
|
||||
ChangedError,
|
||||
NotFoundError,
|
||||
LoopDetectedError
|
||||
LoopDetectedError,
|
||||
ChildDetectedError
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
|
|
Loading…
Reference in a new issue