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