Some basic components for building forms.
This commit is contained in:
parent
d13fc65ce2
commit
4504d539c5
22 changed files with 827 additions and 246 deletions
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"plugins": ["transform-react-jsx", "transform-decorators-legacy"]
|
"presets": ["stage-1"],
|
||||||
|
"plugins": ["transform-react-jsx", "transform-decorators-legacy", "transform-function-bind"]
|
||||||
}
|
}
|
|
@ -15,8 +15,10 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"homepage": "https://mailtrain.org/",
|
"homepage": "https://mailtrain.org/",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.16.1",
|
||||||
"i18next": "^8.3.0",
|
"i18next": "^8.3.0",
|
||||||
"i18next-xhr-backend": "^1.4.1",
|
"i18next-xhr-backend": "^1.4.1",
|
||||||
|
"immutable": "^3.8.1",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^15.5.4",
|
"react": "^15.5.4",
|
||||||
"react-dom": "^15.5.4",
|
"react-dom": "^15.5.4",
|
||||||
|
@ -27,8 +29,10 @@
|
||||||
"babel-cli": "^6.24.1",
|
"babel-cli": "^6.24.1",
|
||||||
"babel-loader": "^7.0.0",
|
"babel-loader": "^7.0.0",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
|
"babel-plugin-transform-function-bind": "^6.22.0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-preset-stage-1": "^6.24.1",
|
||||||
"css-loader": "^0.28.3",
|
"css-loader": "^0.28.3",
|
||||||
"i18next-conv": "^3.0.3",
|
"i18next-conv": "^3.0.3",
|
||||||
"style-loader": "^0.18.1",
|
"style-loader": "^0.18.1",
|
||||||
|
|
247
client/src/lib/form.js
Normal file
247
client/src/lib/form.js
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
import { translate } from 'react-i18next';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from './page.js';
|
||||||
|
|
||||||
|
const FormState = {
|
||||||
|
Loading: 0,
|
||||||
|
LoadingWithNotice: 1,
|
||||||
|
Ready: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
class Form extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
stateOwner: PropTypes.object.isRequired,
|
||||||
|
onSubmit: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
static childContextTypes = {
|
||||||
|
stateOwner: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
stateOwner: this.props.stateOwner
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
const owner = this.props.stateOwner;
|
||||||
|
const props = this.props;
|
||||||
|
|
||||||
|
if (!owner.isFormReady()) {
|
||||||
|
if (owner.isFormWithLoadingNotice()) {
|
||||||
|
return <div>{t('Loading ...')}</div>
|
||||||
|
} else {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<form className="form-horizontal" onSubmit={props.onSubmit}>
|
||||||
|
{props.children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapInput(id, htmlId, owner, label, input) {
|
||||||
|
return (
|
||||||
|
<div className={owner.addFormValidationClass('form-group', id)} >
|
||||||
|
<div className="col-sm-2">
|
||||||
|
<label htmlFor={htmlId} className="control-label">{label}</label>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-10">
|
||||||
|
{input}
|
||||||
|
</div>
|
||||||
|
<div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help'}>{owner.getFormValidationMessage(id)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class InputField extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
placeholder: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
stateOwner: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
const owner = this.context.stateOwner;
|
||||||
|
const id = this.props.id;
|
||||||
|
const htmlId = 'form_' + id;
|
||||||
|
|
||||||
|
return wrapInput(id, htmlId, owner, props.label,
|
||||||
|
<input type="text" value={owner.getFormState(id)} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindToFormState(id)}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextArea extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
placeholder: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
stateOwner: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
const owner = this.context.stateOwner;
|
||||||
|
const id = this.props.id;
|
||||||
|
const htmlId = 'form_' + id;
|
||||||
|
|
||||||
|
return wrapInput(id, htmlId, owner, props.label,
|
||||||
|
<textarea id={htmlId} value={owner.getFormState(id)} className="form-control" aria-describedby={htmlId + '_help'} onChange={owner.bindToFormState(id)}></textarea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ButtonRow extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-10 col-sm-offset-2 mt-button-row">
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function withForm(target) {
|
||||||
|
const inst = target.prototype;
|
||||||
|
|
||||||
|
inst.initFormState = function() {
|
||||||
|
const state = this.state || {};
|
||||||
|
|
||||||
|
state.formState = Immutable.Map({
|
||||||
|
_state: FormState.Loading,
|
||||||
|
_isValidationShown: false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.populateFormStateFromURL = function(url) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState(previousState => {
|
||||||
|
if (previousState.formState.get('_state') === FormState.Loading) {
|
||||||
|
return {
|
||||||
|
formState: previousState.formState.set('_state', FormState.LoadingWithNotice)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
axios.get(url).then(response => {
|
||||||
|
this.populateFormState(response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.populateFormState = function(data) {
|
||||||
|
this.setState(previousState => ({
|
||||||
|
formState: previousState.formState.withMutations(state => {
|
||||||
|
state.set('_state', FormState.Ready);
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
state.set(key, Immutable.Map({
|
||||||
|
value: data[key]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validateFormState(state);
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.updateFormState = function(key, value) {
|
||||||
|
this.setState(previousState => ({
|
||||||
|
formState: previousState.formState.withMutations(state => {
|
||||||
|
state.setIn([key, 'value'], value);
|
||||||
|
this.validateFormState(state);
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.bindToFormState = function(name) {
|
||||||
|
return evt => this.updateFormState(name, evt.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.getFormState = function(name) {
|
||||||
|
return this.state.formState.getIn([name, 'value']);
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.getFormError = function(name) {
|
||||||
|
return this.state.formState.getIn([name, 'error']);
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.isFormWithLoadingNotice = function() {
|
||||||
|
return this.state.formState.get('_state') === FormState.LoadingWithNotice;
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.isFormReady = function() {
|
||||||
|
return this.state.formState.get('_state') === FormState.Ready;
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.isFormValidationShown = function() {
|
||||||
|
return this.state.formState.get('_isValidationShown');
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.addFormValidationClass = function(className, name) {
|
||||||
|
if (this.isFormValidationShown()) {
|
||||||
|
const error = this.getFormError(name);
|
||||||
|
if (error) {
|
||||||
|
return className + ' has-error';
|
||||||
|
} else {
|
||||||
|
return className + ' has-success';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.getFormValidationMessage = function(name) {
|
||||||
|
if (this.isFormValidationShown()) {
|
||||||
|
return this.getFormError(name);
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.showFormValidation = function() {
|
||||||
|
this.setState(previousState => ({formState: previousState.formState.set('_isValidationShown', true)}));
|
||||||
|
};
|
||||||
|
|
||||||
|
inst.hideFormValidation = function() {
|
||||||
|
this.setState(previousState => ({formState: previousState.formState.set('_isValidationShown', false)}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
withForm,
|
||||||
|
Form,
|
||||||
|
InputField,
|
||||||
|
TextArea,
|
||||||
|
ButtonRow,
|
||||||
|
Button
|
||||||
|
}
|
7
client/src/lib/page.css
Normal file
7
client/src/lib/page.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.mt-button-row > button{
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-button-row > button:last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
200
client/src/lib/page.js
Normal file
200
client/src/lib/page.js
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { translate } from 'react-i18next';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'
|
||||||
|
|
||||||
|
import './page.css';
|
||||||
|
|
||||||
|
|
||||||
|
class PageContent extends Component {
|
||||||
|
getRoutes(urlPrefix, children) {
|
||||||
|
let routes = [];
|
||||||
|
for (let routeKey in children) {
|
||||||
|
const structure = children[routeKey];
|
||||||
|
|
||||||
|
let path = urlPrefix + routeKey;
|
||||||
|
|
||||||
|
if (structure.params) {
|
||||||
|
path = path + '/' + structure.params.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structure.component) {
|
||||||
|
const route = {
|
||||||
|
component: structure.component,
|
||||||
|
path: (path === '' ? '/' : path)
|
||||||
|
};
|
||||||
|
|
||||||
|
routes.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structure.children) {
|
||||||
|
routes = routes.concat(this.getRoutes(path + '/', structure.children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRoute(route) {
|
||||||
|
return <Route key={route.path} exact path={route.path} component={route.component} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let routes = this.getRoutes('', this.props.structure);
|
||||||
|
return <Switch>{routes.map(x => this.renderRoute(x))}</Switch>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@withRouter
|
||||||
|
class Breadcrumb extends Component {
|
||||||
|
renderElement(breadcrumbElem) {
|
||||||
|
if (breadcrumbElem.isActive) {
|
||||||
|
return <li key={breadcrumbElem.idx} className="active">{breadcrumbElem.title}</li>;
|
||||||
|
} else if (breadcrumbElem.externalLink) {
|
||||||
|
return <li key={breadcrumbElem.idx}><a href={breadcrumbElem.externalLink}>{breadcrumbElem.title}</a></li>;
|
||||||
|
} else if (breadcrumbElem.link) {
|
||||||
|
return <li key={breadcrumbElem.idx}><Link to={breadcrumbElem.link}>{breadcrumbElem.title}</Link></li>;
|
||||||
|
} else {
|
||||||
|
return <li key={breadcrumbElem.idx}>{breadcrumbElem.title}</li>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const location = this.props.location.pathname;
|
||||||
|
const locationElems = location.split('/');
|
||||||
|
|
||||||
|
let breadcrumbElems = [];
|
||||||
|
let children = this.props.structure;
|
||||||
|
|
||||||
|
for (let idx = 0; idx < locationElems.length; idx++) {
|
||||||
|
const breadcrumbElem = children[locationElems[idx]];
|
||||||
|
if (!breadcrumbElem) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbElem.isActive = (idx === locationElems.length - 1);
|
||||||
|
breadcrumbElem.idx = idx;
|
||||||
|
|
||||||
|
breadcrumbElems.push(breadcrumbElem);
|
||||||
|
children = breadcrumbElem.children;
|
||||||
|
|
||||||
|
if (!children) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedElems = breadcrumbElems.map(x => this.renderElement(x));
|
||||||
|
|
||||||
|
return <ol className="breadcrumb">{renderedElems}</ol>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
class Section extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
let structure = props.structure;
|
||||||
|
if (typeof structure === 'function') {
|
||||||
|
structure = structure(props.t);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.structure = structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div>
|
||||||
|
<Breadcrumb structure={this.structure} />
|
||||||
|
<PageContent structure={this.structure}/>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Title extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{this.props.children}</h2>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Toolbar extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="pull-right mt-button-row">
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Button extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
label: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
type: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
|
||||||
|
let className = 'btn';
|
||||||
|
if (props.className) {
|
||||||
|
className = className + ' ' + props.className;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = props.type || 'button';
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
if (props.icon) {
|
||||||
|
icon = <span className={'glyphicon glyphicon-' + props.icon}></span>
|
||||||
|
}
|
||||||
|
|
||||||
|
let iconSpacer;
|
||||||
|
if (props.icon && props.label) {
|
||||||
|
iconSpacer = ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type={type} className={className} onClick={props.onClick}>{icon}{iconSpacer}{props.label}</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavButton extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
label: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
linkTo: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={props.linkTo}><Button label={props.label} icon={props.icon} className={props.className}/></Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
Section,
|
||||||
|
Title,
|
||||||
|
Toolbar,
|
||||||
|
Button,
|
||||||
|
NavButton
|
||||||
|
};
|
|
@ -1,86 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import {
|
|
||||||
BrowserRouter as Router,
|
|
||||||
Route,
|
|
||||||
Link
|
|
||||||
} from 'react-router-dom'
|
|
||||||
|
|
||||||
import { I18nextProvider, translate } from 'react-i18next';
|
|
||||||
import i18n from './i18n';
|
|
||||||
|
|
||||||
import NamespacesTreeTable from './namespaces/NamespacesTreeTable';
|
|
||||||
|
|
||||||
|
|
||||||
@translate()
|
|
||||||
class List extends Component {
|
|
||||||
render() {
|
|
||||||
console.log(this.props);
|
|
||||||
console.log(this.props.routes);
|
|
||||||
const t = this.props.t;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>{t('Namespaces')}</h2>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<NamespacesTreeTable />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@translate()
|
|
||||||
class Create extends Component {
|
|
||||||
render() {
|
|
||||||
console.log(this.props);
|
|
||||||
console.log(this.props.routes);
|
|
||||||
const t = this.props.t;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>{t('Create Namespace')}</h2>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@translate()
|
|
||||||
class Namespaces extends Component {
|
|
||||||
render() {
|
|
||||||
const t = this.props.t;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Router>
|
|
||||||
<div>
|
|
||||||
<ol className="breadcrumb">
|
|
||||||
<li><a href="/">{t('Home')}</a></li>
|
|
||||||
<li className="active">{t('Namespaces')}</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="pull-right">
|
|
||||||
<Link to="/namespaces/create"><span className="btn btn-primary" role="button"><i className="glyphicon glyphicon-plus"></i> {t('Create Namespace')}</span></Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Route exact path="/namespaces" component={List} />
|
|
||||||
<Route exact path="/namespaces/create" component={Create} />
|
|
||||||
</div>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function() {
|
|
||||||
ReactDOM.render(
|
|
||||||
<I18nextProvider i18n={ i18n }><Namespaces/></I18nextProvider>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
20
client/src/namespaces/Create.js
Normal file
20
client/src/namespaces/Create.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
client/src/namespaces/Edit.js
Normal file
60
client/src/namespaces/Edit.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { translate } from 'react-i18next';
|
||||||
|
import csfrToken from 'csfrToken';
|
||||||
|
import { withForm, Form, InputField, TextArea, ButtonRow, Button } from '../lib/form';
|
||||||
|
import { Title } from "../lib/page";
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
@withForm
|
||||||
|
export default class Edit extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.nsId = parseInt(this.props.match.params.nsId);
|
||||||
|
|
||||||
|
this.initFormState();
|
||||||
|
this.populateFormStateFromURL(`/namespaces/rest/namespaces/${this.nsId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
validateFormState(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submitHandler(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.showFormValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteHandler() {
|
||||||
|
this.hideFormValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title>{t('Edit Namespace')}</Title>
|
||||||
|
|
||||||
|
<Form stateOwner={this} onSubmit={::this.submitHandler}>
|
||||||
|
<InputField id="name" label={t('Name')} description={t('Namespace Name')}/>
|
||||||
|
<TextArea id="description" label={t('Description')} description={t('Description')}/>
|
||||||
|
|
||||||
|
<ButtonRow>
|
||||||
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Update')}/>
|
||||||
|
<Button className="btn-danger" icon="remove" label={t('Delete Namespace')} onClick={::this.deleteHandler}/>
|
||||||
|
</ButtonRow>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
client/src/namespaces/List.js
Normal file
25
client/src/namespaces/List.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { translate } from 'react-i18next';
|
||||||
|
import NamespacesTreeTable from './NamespacesTreeTable';
|
||||||
|
import { Title, Toolbar, NavButton } from "../lib/page";
|
||||||
|
|
||||||
|
@translate()
|
||||||
|
export default class List extends Component {
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Toolbar>
|
||||||
|
<NavButton linkTo="/namespaces/create" className="btn-primary" icon="plus" label={t('Create Namespace')}/>
|
||||||
|
</Toolbar>
|
||||||
|
|
||||||
|
<Title>{t('Namespaces')}</Title>
|
||||||
|
|
||||||
|
<NamespacesTreeTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,30 +7,29 @@ import jQuery from 'jquery';
|
||||||
import '../../public/jquery/jquery-ui-1.12.1.min.js';
|
import '../../public/jquery/jquery-ui-1.12.1.min.js';
|
||||||
import '../../public/fancytree/jquery.fancytree-all.min.js';
|
import '../../public/fancytree/jquery.fancytree-all.min.js';
|
||||||
import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
|
import '../../public/fancytree/skin-bootstrap/ui.fancytree.min.css';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
translate();
|
@translate()
|
||||||
class NamespacesTreeTable extends Component {
|
export default class NamespacesTreeTable extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
treeData: [
|
treeData: []
|
||||||
{title: 'A', key: '1', expanded: true},
|
|
||||||
{title: 'B', key: '2', expanded: true, folder: true, children: [
|
|
||||||
{title: 'BA', key: '3', expanded: true, folder: true, children: [
|
|
||||||
{title: 'BAA', key: '4', expanded: true},
|
|
||||||
{title: 'BAB', key: '5', expanded: true}
|
|
||||||
]},
|
|
||||||
{title: 'BB', key: '6', expanded: true, folder: true, children: [
|
|
||||||
{title: 'BBA', key: '7', expanded: true},
|
|
||||||
{title: 'BBB', key: '8', expanded: true}
|
|
||||||
]}
|
|
||||||
]}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
axios.get('/namespaces/rest/namespacesTree')
|
||||||
|
.then(response => {
|
||||||
|
this.setState({
|
||||||
|
treeData: response.data
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const history = this.props.history;
|
||||||
|
|
||||||
const glyphOpts = {
|
const glyphOpts = {
|
||||||
map: {
|
map: {
|
||||||
expanderClosed: 'glyphicon glyphicon-menu-right',
|
expanderClosed: 'glyphicon glyphicon-menu-right',
|
||||||
|
@ -49,8 +48,15 @@ class NamespacesTreeTable extends Component {
|
||||||
source: this.state.treeData,
|
source: this.state.treeData,
|
||||||
table: {
|
table: {
|
||||||
nodeColumnIdx: 0
|
nodeColumnIdx: 0
|
||||||
|
},
|
||||||
|
createNode: (event, data) => {
|
||||||
|
const node = data.node;
|
||||||
|
const tdList = jQuery(node.tr).find(">td");
|
||||||
|
tdList.eq(1).html('<a href="#">Edit</a>').click(() => {
|
||||||
|
history.push('/namespaces/edit/' + node.key);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
}).fancytree("getTree");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -58,13 +64,15 @@ class NamespacesTreeTable extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
const container =
|
const container =
|
||||||
<div ref={(domElem) => { this.domTableContainer = domElem; }} style={{ height: '100px', overflow: 'auto'}}>
|
<div ref={(domElem) => { this.domTableContainer = domElem; }} style={{ height: '100px', overflow: 'auto'}}>
|
||||||
<table ref={(domElem) => { this.domTable = domElem; }} className="table table-hover table-striped table-condensed">
|
<table ref={(domElem) => { this.domTable = domElem; }} className="table table-hover table-striped table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>{t('Name')}</th>
|
||||||
<th>B</th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -81,6 +89,4 @@ class NamespacesTreeTable extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NamespacesTreeTable;
|
|
46
client/src/namespaces/root.js
Normal file
46
client/src/namespaces/root.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import i18n from '../lib/i18n';
|
||||||
|
|
||||||
|
import { Section } from '../lib/page'
|
||||||
|
import Create from './Create'
|
||||||
|
import Edit from './Edit'
|
||||||
|
import List from './List'
|
||||||
|
|
||||||
|
const getStructure = t => ({
|
||||||
|
'': {
|
||||||
|
title: t('Home'),
|
||||||
|
externalLink: '/',
|
||||||
|
children: {
|
||||||
|
'namespaces': {
|
||||||
|
title: t('Namespaces'),
|
||||||
|
link: '/namespaces',
|
||||||
|
component: List,
|
||||||
|
children: {
|
||||||
|
'edit' : {
|
||||||
|
title: t('Edit Namespace'),
|
||||||
|
params: [':nsId'],
|
||||||
|
component: Edit
|
||||||
|
},
|
||||||
|
'create' : {
|
||||||
|
title: t('Create Namespace'),
|
||||||
|
link: '/namespaces/create',
|
||||||
|
component: Create
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
ReactDOM.render(
|
||||||
|
<I18nextProvider i18n={ i18n }><Section structure={getStructure}/></I18nextProvider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ const path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
namespaces: './src/namespaces.js'
|
namespaces: './src/namespaces/root.js'
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
library: 'MailtrainReactBody',
|
library: 'MailtrainReactBody',
|
||||||
|
@ -18,7 +18,8 @@ module.exports = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
jquery: 'jQuery'
|
jquery: 'jQuery',
|
||||||
|
csfrToken: 'csfrToken'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// new webpack.optimize.UglifyJsPlugin(),
|
// new webpack.optimize.UglifyJsPlugin(),
|
||||||
|
|
11
lib/InteroperableError.js
Normal file
11
lib/InteroperableError.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
class InteroperableError extends Error {
|
||||||
|
constructor(type, msg, data) {
|
||||||
|
super(msg);
|
||||||
|
this.type = type;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = InteroperableError;
|
|
@ -23,7 +23,8 @@ module.exports = {
|
||||||
injectCustomFormTemplates,
|
injectCustomFormTemplates,
|
||||||
filterCustomFields,
|
filterCustomFields,
|
||||||
getMjmlTemplate,
|
getMjmlTemplate,
|
||||||
rollbackAndReleaseConnection
|
rollbackAndReleaseConnection,
|
||||||
|
filterObject
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDefaultMergeTags(callback) {
|
function getDefaultMergeTags(callback) {
|
||||||
|
@ -293,3 +294,14 @@ function rollbackAndReleaseConnection(connection, callback) {
|
||||||
return callback();
|
return callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterObject(obj, allowedKeys) {
|
||||||
|
const result = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (allowedKeys.has(key)) {
|
||||||
|
result[key] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,76 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const knex = require('../knex');
|
const knex = require('../knex');
|
||||||
|
const hasher = require('node-object-hash')();
|
||||||
|
const { filterObject } = require('../helpers');
|
||||||
|
const InteroperableError = require('../InteroperableError');
|
||||||
|
|
||||||
module.exports.list = () => knex('namespaces');
|
class ChangedError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('namespaces.ChangedError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotFoundError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('namespaces.NotFoundError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedKeys = new Set(['id', 'name', 'description', 'parent']);
|
||||||
|
const allowedUpdateKeys = new Set(['name', 'description', 'parent']);
|
||||||
|
|
||||||
|
async function list() {
|
||||||
|
return await knex('namespaces');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(ns) {
|
||||||
|
return hasher.hash(filterObject(ns, allowedKeys));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getById(nsId) {
|
||||||
|
const ns = await knex('namespaces').where('id', nsId).first();
|
||||||
|
if (!ns) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
ns.hash = hash(ns);
|
||||||
|
|
||||||
|
return ns;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(ns) {
|
||||||
|
const nsId = await knex('namespaces').insert(filterObject(ns, allowedKeys));
|
||||||
|
return nsId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWithConsistencyCheck(ns) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
const existingNs = await tx('namespaces').where('id', ns.id).first();
|
||||||
|
if (!ns) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingNsHash = hash(existingNs);
|
||||||
|
if (existingNsHash != ns.originalHash) {
|
||||||
|
throw new ChangedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
tx('namespaces').where('id', ns.id).update(filterObject(ns, allowedUpdateKeys));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(nsId) {
|
||||||
|
await knex('namespaces').where('id', nsId).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
NotFoundError,
|
||||||
|
ChangedError,
|
||||||
|
hash,
|
||||||
|
list,
|
||||||
|
getById,
|
||||||
|
create,
|
||||||
|
updateWithConsistencyCheck,
|
||||||
|
remove
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const InteroperableError = require('./InteroperableError');
|
||||||
|
|
||||||
function safeHandler(handler) {
|
function safeHandler(handler) {
|
||||||
return function(req, res) {
|
return function(req, res) {
|
||||||
|
@ -8,12 +9,45 @@ function safeHandler(handler) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeJSONHandler(handler) {
|
||||||
|
return function(req, res) {
|
||||||
|
handler(req, res).catch(error => {
|
||||||
|
const resp = {
|
||||||
|
message: error.message
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error instanceof InteroperableError) {
|
||||||
|
resp.type = error.type;
|
||||||
|
resp.data = error.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json(resp);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceLast(elems, replaceFn) {
|
||||||
|
if (elems.length === 0) {
|
||||||
|
return elems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastElem = elems[elems.size - 1];
|
||||||
|
const replacement = replaceFn(lastElem);
|
||||||
|
|
||||||
|
elems[elems.size - 1] = replacement;
|
||||||
|
|
||||||
|
return elems;
|
||||||
|
}
|
||||||
|
|
||||||
function create() {
|
function create() {
|
||||||
const router = new express.Router();
|
const router = new express.Router();
|
||||||
|
|
||||||
router.getAsync = (path, asyncFn) => router.get(path, safeHandler(asyncFn));
|
router.getAsync = (path, ...handlers) => router.get(path, ...replaceLast(handlers, safeHandler));
|
||||||
|
|
||||||
router.postAsync = (path, asyncFn) => router.post(path, safeHandler(asyncFn));
|
router.getAsyncJSON = (path, ...handlers) => router.get(path, ...replaceLast(handlers, safeJSONHandler));
|
||||||
|
router.postAsyncJSON = (path, ...handlers) => router.post(path, ...replaceLast(handlers, safeJSONHandler));
|
||||||
|
router.putAsyncJSON = (path, ...handlers) => router.put(path, ...replaceLast(handlers, safeJSONHandler));
|
||||||
|
router.deleteAsyncJSON = (path, ...handlers) => router.delete(path, ...replaceLast(handlers, safeJSONHandler));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
"mysql": "^2.13.0",
|
"mysql": "^2.13.0",
|
||||||
"node-gettext": "^2.0.0-rc.1",
|
"node-gettext": "^2.0.0-rc.1",
|
||||||
"node-mocks-http": "^1.6.1",
|
"node-mocks-http": "^1.6.1",
|
||||||
|
"node-object-hash": "^1.2.0",
|
||||||
"nodemailer": "^3.1.8",
|
"nodemailer": "^3.1.8",
|
||||||
"nodemailer-openpgp": "^1.0.2",
|
"nodemailer-openpgp": "^1.0.2",
|
||||||
"npmlog": "^4.0.2",
|
"npmlog": "^4.0.2",
|
||||||
|
|
|
@ -14,15 +14,50 @@ router.all('/*', (req, res, next) => {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/', async (req, res) => {
|
router.getAsyncJSON('/rest/namespaces/:nsId', async (req, res) => {
|
||||||
res.render('react-root', {
|
const ns = await namespaces.getById(req.params.nsId);
|
||||||
title: _('Namespaces'),
|
return res.json(ns);
|
||||||
reactEntryPoint: 'namespaces'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/list/ajax', async (req, res) => {
|
router.postAsyncJSON('/rest/namespaces', passport.csrfProtection, async (req, res) => {
|
||||||
|
console.log(req.body);
|
||||||
|
// await namespaces.create(req.body);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.putAsyncJSON('/rest/namespaces/:nsId', passport.csrfProtection, async (req, res) => {
|
||||||
|
console.log(req.body);
|
||||||
|
ns = req.body;
|
||||||
|
ns.id = req.params.nsId;
|
||||||
|
|
||||||
|
// await namespaces.updateWithConsistencyCheck(ns);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.deleteAsyncJSON('/rest/namespaces/:nsId', passport.csrfProtection, async (req, res) => {
|
||||||
|
console.log(req.body);
|
||||||
|
// await namespaces.remove(req.params.nsId);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.getAsyncJSON('/rest/namespacesTree', async (req, res) => {
|
||||||
const entries = {};
|
const entries = {};
|
||||||
|
|
||||||
|
/* Example of roots:
|
||||||
|
[
|
||||||
|
{title: 'A', key: '1', expanded: true},
|
||||||
|
{title: 'B', key: '2', expanded: true, folder: true, children: [
|
||||||
|
{title: 'BA', key: '3', expanded: true, folder: true, children: [
|
||||||
|
{title: 'BAA', key: '4', expanded: true},
|
||||||
|
{title: 'BAB', key: '5', expanded: true}
|
||||||
|
]},
|
||||||
|
{title: 'BB', key: '6', expanded: true, folder: true, children: [
|
||||||
|
{title: 'BBA', key: '7', expanded: true},
|
||||||
|
{title: 'BBB', key: '8', expanded: true}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
*/
|
||||||
const roots = [];
|
const roots = [];
|
||||||
|
|
||||||
const rows = await namespaces.list();
|
const rows = await namespaces.list();
|
||||||
|
@ -58,6 +93,12 @@ router.getAsync('/list/ajax', async (req, res) => {
|
||||||
return res.json(roots);
|
return res.json(roots);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('')
|
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
|
||||||
|
res.render('react-root', {
|
||||||
|
title: _('Namespaces'),
|
||||||
|
reactEntryPoint: 'namespaces',
|
||||||
|
reactCsrfToken: req.csrfToken()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -21,6 +21,11 @@
|
||||||
<script src="/javascript/jquery-2.2.1.min.js"></script>
|
<script src="/javascript/jquery-2.2.1.min.js"></script>
|
||||||
<script src="/bootstrap/js/bootstrap.min.js"></script>
|
<script src="/bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="/mailtrain/common.js"></script>
|
<script src="/mailtrain/common.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.csfrToken = '{{reactCsrfToken}}';
|
||||||
|
</script>
|
||||||
|
|
||||||
<script src="/mailtrain/{{reactEntryPoint}}.js"></script>
|
<script src="/mailtrain/{{reactEntryPoint}}.js"></script>
|
||||||
{{else}}
|
{{else}}
|
||||||
<link rel="stylesheet" href="/bootstrap/themes/united.min.css">
|
<link rel="stylesheet" href="/bootstrap/themes/united.min.css">
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
|
||||||
<li>{{#translate}}Namespaces{{/translate}}</li>
|
|
||||||
<li class="active">{{#translate}}Edit Namespace{{/translate}}</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div class="pull-right">
|
|
||||||
<a class="btn btn-primary" href="/reports/create" role="button"><i class="glyphicon glyphicon-plus"></i> {{#translate}}Create Namespace{{/translate}}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>{{#translate}}Namespaces{{/translate}}</h2>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<form method="post" class="delete-form" id="namespaces-delete" action="/namespaces/delete">
|
|
||||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
|
||||||
<input type="hidden" name="id" value="{{id}}" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="form-horizontal" method="post" action="/namespaces/edit">
|
|
||||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
|
||||||
<input type="hidden" name="id" value="{{id}}" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
|
||||||
<div class="pull-right">
|
|
||||||
<button type="submit" form="namespaces-delete" class="btn btn-danger"><i class="glyphicon glyphicon-remove"></i> {{#translate}}Delete Namespace{{/translate}}</button>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
|
@ -1,86 +0,0 @@
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
|
||||||
<li class="active">{{#translate}}Namespaces{{/translate}}</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div class="pull-right">
|
|
||||||
<a class="btn btn-primary" href="/reports/create" role="button"><i class="glyphicon glyphicon-plus"></i> {{#translate}}Create Namespace{{/translate}}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>{{#translate}}Namespaces{{/translate}}</h2>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div id="treetable-container" style="height: 100px; overflow: auto;">
|
|
||||||
<table id="treetable" class="table table-hover table-striped table-condensed">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>B</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
|
|
||||||
glyph_opts = {
|
|
||||||
map: {
|
|
||||||
expanderClosed: 'glyphicon glyphicon-menu-right',
|
|
||||||
expanderLazy: 'glyphicon glyphicon-menu-right', // glyphicon-plus-sign
|
|
||||||
expanderOpen: 'glyphicon glyphicon-menu-down', // glyphicon-collapse-down
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
$('#treetable').fancytree({
|
|
||||||
extensions: ['glyph', 'table'],
|
|
||||||
glyph: glyph_opts,
|
|
||||||
selectMode: 1,
|
|
||||||
icon: false,
|
|
||||||
autoScroll: true,
|
|
||||||
scrollParent: $("#treetable-container"),
|
|
||||||
source: [
|
|
||||||
{title: 'A', key: '1', expanded: true},
|
|
||||||
{title: 'B', key: '2', expanded: true, folder: true, children: [
|
|
||||||
{title: 'BA', key: '3', expanded: true, folder: true, children: [
|
|
||||||
{title: 'BAA', key: '4', expanded: true},
|
|
||||||
{title: 'BAB', key: '5', expanded: true}
|
|
||||||
]},
|
|
||||||
{title: 'BB', key: '6', expanded: true, folder: true, children: [
|
|
||||||
{title: 'BBA', key: '7', expanded: true},
|
|
||||||
{title: 'BBB', key: '8', expanded: true}
|
|
||||||
]}
|
|
||||||
]}
|
|
||||||
],
|
|
||||||
table: {
|
|
||||||
nodeColumnIdx: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
$('#treetable').fancytree({
|
|
||||||
extensions: ['glyph', 'table'],
|
|
||||||
glyph: glyph_opts,
|
|
||||||
selectMode: 1,
|
|
||||||
icon: false,
|
|
||||||
autoScroll: true,
|
|
||||||
scrollParent: $("#treetable-container"),
|
|
||||||
source: {
|
|
||||||
url: "/namespaces/list/ajax",
|
|
||||||
cache: true
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
nodeColumnIdx: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
Loading…
Add table
Add a link
Reference in a new issue