anubis/frontend/src/app.js
// Copyright © 2016, Ugo Pozo
// 2016, Câmara Municipal de São Paulo
// app.js - main component of the search interface.
// This file is part of Anubis.
// Anubis is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Anubis is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, {PropTypes as RPropTypes} from 'react';
import I from 'immutable';
import IPropTypes from 'react-immutable-proptypes';
import _ from 'lodash';
import AppTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import {Paper, RaisedButton, Dialog, Snackbar, ListItem, Divider,
CircularProgress, Drawer, List, Subheader} from 'material-ui';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {StickyContainer} from 'react-sticky';
import Actions from 'actions';
import Header from 'components/header';
import Footer from 'components/footer';
import TokenField from 'components/TokenField';
import buildField from 'components/build_field';
const getStateProps = state => ({
appTitle: state.getIn(['applicationData', 'title']),
appTheme: state.getIn(['templates', 'appTheme']),
routing: state.get('routing'),
baseURL: state.getIn(['applicationData', 'baseURL']),
detailsHtml: state.getIn(['applicationData', 'detailsHtml']),
searchHtml: state.getIn(['applicationData', 'searchHtml']),
searchApi: state.getIn(['applicationData', 'searchApi']),
globalError: state.getIn(['applicationData', 'globalError']),
showErrorDetails: state.getIn(['applicationData', 'showErrorDetails']),
actions: state.getIn(['searchResults', 'actions']),
currentAction: state.getIn(['applicationData', 'currentAction'], null),
actionResult: state.getIn(['searchResults', 'actionResult']),
results: state.getIn(['searchResults', 'results']),
sidebarLinks: state.getIn(['applicationData', 'sidebarLinks']),
user: state.get('user'),
modelName: state.getIn(['searchResults', 'model']),
noUserText: state.get('noUserText'),
});
const getDispatchProps = dispatch => ({
clearGlobalError: bindActionCreators(Actions.clearGlobalError, dispatch),
showGlobalErrorDetails: bindActionCreators(Actions.showGlobalErrorDetails,
dispatch),
cancelServerAction: bindActionCreators(Actions.cancelServerAction,
dispatch),
submitServerAction: bindActionCreators(Actions.submitServerAction,
dispatch),
});
@connect(getStateProps, getDispatchProps)
export default class App extends React.Component {
static propTypes = {
appTheme: IPropTypes.map,
appTitle: RPropTypes.string,
extra: RPropTypes.object,
location: RPropTypes.shape({
pathname: RPropTypes.string,
}),
modelName: RPropTypes.string.isRequired,
sidebarLinks: IPropTypes.contains({
admin: RPropTypes.string,
list: IPropTypes.list,
login: RPropTypes.string,
logout: RPropTypes.string,
title: RPropTypes.string,
}),
user: IPropTypes.contains({
email: RPropTypes.string,
first_name: RPropTypes.string,
last_name: RPropTypes.string,
profile_link: RPropTypes.string,
username: RPropTypes.string,
}),
noUserText: RPropTypes.string,
}
static childContextTypes = {
muiTheme: RPropTypes.object,
}
constructor(props) {
super(props);
this.state = {
dataSource: [],
waiting: false,
showNav: false,
};
this.actionArgs = null;
}
getChildContext() {
return {
muiTheme: this.theme,
};
}
get theme() {
return (this.props.appTheme) ?
getMuiTheme(this.props.appTheme.toJS()) :
getMuiTheme(AppTheme);
}
detailsHtml(model, id) {
return eval("`" + this.props.detailsHtml + "`");
}
searchHtml(model, page, sorting, expr) {
return eval("`" + this.props.searchHtml + "`");
}
get searchApi() {
/*eslint-disable no-unused-vars */
const model = this.props.params.model;
const expr = this.props.params.splat;
const page = this.props.params.page;
const sorting = this.props.params.sorting;
/*eslint-enable no-unused-vars */
return eval("`" + this.props.searchApi + "`");
}
renderError() {
let error = this.props.globalError;
const showErrorDialog = !!error && this.props.showErrorDetails;
const showErrorSnackbar = !!error && !this.props.showErrorDetails;
if (!error) error = I.Map();
const traceback = error.get('traceback', null);
return (
<div>
<Dialog
actions={
<RaisedButton
label="Fechar"
onTouchTap={() => this.props.clearGlobalError()}
primary
/>
}
actionsContainerStyle={{borderTop: 'none'}}
autoScrollBodyContent
modal
open={showErrorDialog}
title={error.get('name')}
titleStyle={{borderBottom: 'none'}}
>
<p style={{marginBottom: "20px"}}>{error.get('detail')}</p>
{traceback && (
<div>
<h4 style={{marginBottom: "20px"}}>
{`Detalhes:`}
</h4>
<Paper
style={{
padding: "20px",
color: this.theme.flatButton.textColor,
backgroundColor: this.theme.toolbar.
backgroundColor,
}}
zDepth={1}
>
<code
style={{
whiteSpace: "pre-wrap",
fontFamily: "'Roboto Mono', monospace",
fontSize: "12pt",
}}
>
{traceback.map((line, i) => (
<span key={`line_${i}`}>
{line}
</span>
)).toJS()}
</code>
</Paper>
</div>
)}
</Dialog>
<Snackbar
action="Detalhes..."
autoHideDuration={5000}
message={`Erro: ${error.get('name')}`}
onActionTouchTap={() => {
this.props.showGlobalErrorDetails();
}}
onRequestClose={() => this.props.clearGlobalError()}
open={showErrorSnackbar}
style={{
fontFamily: "'Roboto', sans-serif",
fontSize: "16pt",
}}
/>
</div>
);
}
handleSubmitAction = () => {
if (!this.props.currentAction) return;
if (!this.actionArgs) return;
const actionData = this.props.actions.get(this.props.currentAction);
const fields = actionData.get('fields');
const args = new FormData();
const results = this.props.results.map(result => result.get('id'));
args.set('object_list', JSON.stringify(results.toArray()));
args.set('action_name', this.props.currentAction);
for (const key of fields.keys()) {
if (_.has(this.actionArgs, key)) {
args.set(key, this.actionArgs[key]);
}
}
this.props.submitServerAction(this.searchApi, args).then().then(() => {
this.setState({waiting: false});
});
this.setState({waiting: true});
}
renderAction() {
const open = !!this.props.currentAction;
let actionData = I.fromJS({});
if (open) {
actionData = this.props.actions.get(this.props.currentAction);
if (!this.actionArgs) {
this.actionArgs = {};
}
}
else {
this.actionArgs = null;
}
const description = actionData.get('description', null);
const fields = actionData.get('fields', I.Map());
const hasResult = !!this.props.actionResult;
const title = actionData.get('title', null);
const resultSuccess = (hasResult) ?
this.props.actionResult.get('success') : false;
const resultError = (hasResult) ?
this.props.actionResult.get('error') : null;
const result = (hasResult) ? this.props.actionResult.get('result') :
null;
return (
<Dialog
actions={
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "6px",
}}
>
<RaisedButton
label={"Fechar"}
onTouchTap={this.props.cancelServerAction}
secondary
/>
{!hasResult && (
<RaisedButton
label="OK"
onTouchTap={this.handleSubmitAction}
primary
/>
)}
</div>
}
actionsContainerStyle={{borderTop: 'none'}}
autoScrollBodyContent
modal
onRequestClose={this.props.cancelServerAction}
open={open}
title={title}
titleStyle={{borderBottom: 'none'}}
>
{this.state.waiting && (
<div style={{textAlign: "center"}}>
<CircularProgress
size={80}
thickness={7}
/>
</div>
) || (
<div>
{hasResult && (
<div>
{resultSuccess
&& (
<div>
<p>{"Successo"}</p>
<p>{result}
</p>
</div>
) || (
<div>
<p>{"Erro"}</p>
<p>{resultError}</p>
</div>
)}
</div>
) || (
<div>
<p>{description}</p>
<p>{fields.map((field, key) => (
buildField(field, {
onUpdateInput: searchText => {
let url = field
.get('autocomplete_url');
url += encodeURIComponent(
searchText);
const completion = fetch(url, {
credentials: 'same-origin',
});
completion.then(r => r.json()
.then(json => {
this.setState({
dataSource: json,
});
})
);
},
onSelect: (_, which) => {
this.actionArgs[key] = this.state
.dataSource[which][0];
},
onClearInput: () => {
this.setState({dataSource: []});
},
dataSource: this.state.dataSource.
map(([_, value]) => value),
key,
})
)).valueSeq().toArray()}</p>
</div>
)}
</div>
)}
</Dialog>
);
}
handleToggleNav = () => {
this.setState({showNav: !this.state.showNav});
}
renderUserInfo() {
let userShow;
const firstName = this.props.user.get('first_name', "");
if (firstName != "") {
const lastName = this.props.user.get('last_name', "");
userShow = `${firstName} ${lastName}`;
}
else {
userShow = `${this.props.user.get('username')}`;
}
const userEmail = this.props.user.get('email');
const location = this.props.location.pathname;
const logoutLink = `${this.props.sidebarLinks.get('logout')}?` +
`next=` + encodeURIComponent(location);
let menuItems = [
{
text: 'Administração',
href: this.props.sidebarLinks.get('admin'),
},
{
text: 'Perfil',
href: this.props.user.get('profile_link'),
},
{
text: 'Alterar senha',
href: this.props.sidebarLinks.get('password'),
},
{
sep: true,
},
{
text: 'Sair',
href: logoutLink,
noBlank: true,
},
];
menuItems = menuItems.map((item, i) => {
if (item.sep) {
return (
<ListItem
key={`item_${i}`}
>
<Divider
style={{marginLeft: 36}}
/>
</ListItem>
);
}
return (
<ListItem
href={item.href}
key={`item_${i}`}
style={{paddingLeft: 36}}
target={(item.noBlank) ? '' : '_blank'}
>
{item.text}
</ListItem>
);
});
return (
<ListItem
nestedItems={menuItems}
primaryText={userShow}
secondaryText={userEmail}
/>
);
}
renderLogin() {
const location = this.props.location.pathname;
const loginLink = `${this.props.sidebarLinks.get('login')}?next=` +
encodeURIComponent(location);
return (
<ListItem
href={loginLink}
primaryText={`Entrar`}
/>
);
}
renderNoUser() {
return (
<ListItem primaryText={this.props.noUserText} />
);
}
render() {
let navHeader;
if (I.is(this.props.user, I.Map())) {
navHeader = this.renderLogin();
}
else if (this.props.user === null) {
navHeader = this.renderNoUser();
}
else {
navHeader = this.renderUserInfo();
}
return (
<div>
<Header
onRequestToggle={this.handleToggleNav}
/>
<Drawer
containerStyle={{zIndex: 9510}}
docked={false}
onRequestChange={this.handleToggleNav}
open={this.state.showNav}
overlayStyle={{zIndex: 9500}}
>
<List
style={{
paddingTop: 48,
}}
>
{navHeader}
</List>
<Divider />
<List>
{this.props.sidebarLinks.get('list').map(([t, l]) => (
<ListItem
primaryText={t}
href={l}
key={l}
target="_blank"
/>
)).unshift((
<Subheader key={`__SUBHEADER`}>
{this.props.sidebarLinks.get('title')}
</Subheader>
)).toJS()}
</List>
</Drawer>
{this.props.extra}
<TokenField params={this.props.params} />
<StickyContainer>
{this.props.list}
</StickyContainer>
{this.props.zoom}
{this.renderError()}
{this.renderAction()}
<Footer />
</div>
);
}
}