application/components/app.js
import React from 'react';
import {connect} from 'react-redux';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import ceoTheme from './../theme';
import spacing from 'material-ui/styles/spacing';
import IconDashboard from 'material-ui/svg-icons/action/dashboard';
import IconCreate from 'material-ui/svg-icons/content/create';
import IconAdd from 'material-ui/svg-icons/content/add';
import IconHistory from 'material-ui/svg-icons/action/history';
import IconEvent from 'material-ui/svg-icons/action/event';
import IconQuilt from 'material-ui/svg-icons/action/view-quilt';
import FontIcon from 'material-ui/FontIcon';
import AppBar from 'material-ui/AppBar';
import Drawer from 'material-ui/Drawer';
import Paper from 'material-ui/Paper';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import List from 'material-ui/List/List';
import ListItem from 'material-ui/List/ListItem';
import Avatar from 'material-ui/Avatar';
import Popover from 'material-ui/Popover';
import FlatButton from 'material-ui/FlatButton';
import Divider from 'material-ui/Divider';
import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar';
import IconMenu from 'material-ui/IconMenu';
import IconButton from 'material-ui/IconButton';
import TextField from 'material-ui/TextField';
import NavigationExpandMoreIcon from 'material-ui/svg-icons/navigation/expand-more';
import TopLevelErrorBoundary from '../error-boundaries/top-level';
import {Link, browserHistory} from 'react-router';
import CurrentUser from './../current-user';
import Config from './../config';
import RootExtension from './layout/root';
import ErrorViewer from './common/errors';
import Spashy from './common/splashy';
import DropzoneContainer from './common/dropzone';
import UploadCart from './common/upload-cart';
import Quips from './common/quips';
import NotificationBar from './notification-bar';
import SystemAlerts from './system-alerts';
import ChangeLog from './changelog';
import UserChangePassword from './user-change-password';
import CeoAppBar from './layout/ceo-appbar'
import {info} from './../util/console';
import {truncate} from './../util/strings';
import URI from 'urijs';
import {
contentMaybeFetchCache
} from './../redux/actions/content-actions';
import {
assignmentsMaybeFetchCache
} from './../redux/actions/assignment-actions';
import {
applicationDidNotifyUpdates
} from './../redux/actions/application-actions';
import {
userUpdateMe
} from './../redux/actions/user-actions';
import {
snackbarShowMessage
} from './../redux/actions/snackbar-actions';
// import Flasher from './flasher';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
i: 0,
shouldStart: false,
height: 1000,
drawerWidth: spacing.desktopKeylineIncrement * 4,
drawerIsOpen: false,
popoverIsOpen: false,
popoverAnchor: null,
firstPopover: false,
secondPopover: false,
historyPopover: false,
pluginPopover: false,
userPasswordModalOpen: false,
userPasswordError: false,
isInternalDrag: false
};
info('--- CEO REACT START ----');
this.handleTick = this.handleTick.bind(this);
this.handleDragEnter = this.handleDragEnter.bind(this);
this.handleUploadFinish = this.handleUploadFinish.bind(this);
this.handleUploadCancel = this.handleUploadCancel.bind(this);
this.handleResize = this.handleResize.bind(this);
}
componentDidMount() {
this.setState({shouldStart: true});
this.handleResize();
if (window && window.document) {
const el = window.document.getElementById('app-splash');
el.setAttribute('class', 'end');
setTimeout(() => {
el.remove();
}, 500);
}
if (this.props.router.location && this.props.router.location.query._fpwm) {
this.setState({'userPasswordModalOpen': true});
}
}
componentWillMount() {
if (window) {
window.addEventListener('resize', this.handleResize);
}
const {dispatch} = this.props;
// dispatch(contentMaybeFetchCache({'by_type': 1}));
// dispatch(assignmentsMaybeFetchCache());
// // alerts the user that reloading may cause data loss
window.addEventListener('beforeunload', (e) => {
if (this.props.applicationState.isDirty) {
e.returnValue = '\m/';
}
this.props.globalHistory.items.map((item) => {
if (item.get('isDirty')) {
// this can be literally any string, which is funny...
e.returnValue = '\m/';
}
})
});
}
componentWillUnmount() {
if (window) {
window.removeEventListener('resize', this.handleResize);
}
}
handleResize() {
if (!window) {
return;
}
const height = window.innerHeight;
if (!height) {
return;
}
this.setState({'height': height});
}
handleTick() {
this.setState({shouldStart: true});
}
handleInternalStart(proxy, e) {
this.setState({'isInternalDrag': true});
}
handleInternalEnd(proxy, e) {
this.setState({'isInternalDrag': false});
}
handleDragEnter(proxy, e) {
if (this.state.isInternalDrag) {
return;
}
this.setState({shouldShowDropzone: true});
}
handleUploadFinish(e) {
this.setState({shouldShowDropzone: false});
}
handleUploadCancel(e) {
this.setState({shouldShowDropzone: false});
}
toggleDrawer() {
if (this.state.drawerIsOpen) {
this.setState({'drawerIsOpen': false});
} else {
this.setState({'drawerIsOpen': true});
}
}
navigateTo(path) {
this.props.router.push(path);
if (this.state.drawerIsOpen) {
this.toggleDrawer();
}
}
togglePopover(popoverName, e) {
var change = {};
change[popoverName] = this.state[popoverName] ? false : true;
// prevents the menu from "jumping" on close
if (change[popoverName]) {
change['popoverAnchor'] = e.currentTarget;
}
this.setState(change);
}
handleHistoryLink(item, e) {
this.togglePopover.call(this, 'historyPopover');
if (item.get('type') == 'content') {
browserHistory.push('/ceo/locate/' + item.get('uuid'));
} else {
browserHistory.push('/ceo/locate/' + item.get('uuid') + '?type=' + item.get('type'));
}
}
handleRequestUpload(e) {
this.setState({
firstPopover: false,
shouldShowDropzone: true
});
}
handleChangelogClose(e) {
const {dispatch} = this.props;
dispatch(applicationDidNotifyUpdates());
}
handleRequestPasswordChange(e) {
this.setState({userPasswordModalOpen: true, userPasswordError: false});
}
handleRequestPasswordChangeClose(e) {
this.setState({userPasswordModalOpen: false, userPasswordError: false});
}
handleUpdatePassword(existingPass, newPass) {
const {dispatch} = this.props;
if (!existingPass || !newPass) {
dispatch(snackbarShowMessage('You must provide both current and new passwords.'));
return;
}
dispatch(userUpdateMe({'checkPassword': existingPass, 'password': newPass}))
.then((user) => {
dispatch(snackbarShowMessage('Password updated'));
this.setState({userPasswordModalOpen: false, userPasswordError: false});
})
.catch((e) => {
this.setState({userPasswordError: 'Please provide your current password'});
dispatch(snackbarShowMessage('There was a problem updating your password.'));
return;
});
}
getIcon(type) {
const faStyles = {
'fontSize': '22px', //
'width': '22px', // Drop the font size by 2 px
'height': '22px', //
'left': '26px' // but move it over to the right 2 px
};
switch (type) {
case 'article':
return <FontIcon className='fa fa-pencil-square-o' style={faStyles}></FontIcon>;
case 'media':
return <FontIcon className='mui-icons'>photo</FontIcon>
case 'post':
return <FontIcon className='mui-icons'>question_answer</FontIcon>;
case 'page':
return <FontIcon className='mui-icons'>description</FontIcon>;
case 'container':
return <FontIcon className='mui-icons'>view_module</FontIcon>;
case 'assignment':
return <FontIcon className='mui-icons'>event</FontIcon>;
}
}
render() {
if (!this.state.shouldStart) {
return (
<MuiThemeProvider muiTheme={getMuiTheme(ceoTheme)}>
<Spashy />
</MuiThemeProvider>
);
}
let dropzone = '';
if (this.state.shouldShowDropzone) {
dropzone = <DropzoneContainer onFinish={this.handleUploadFinish} onCancel={this.handleUploadCancel} />;
}
let searchUrl = '/ceo/search';
if (this.props.globalSearch.navigationQuery) {
searchUrl += '?' + URI(URI.buildQuery(this.props.globalSearch.navigationQuery))
.duplicateQueryParameters(false)
.normalizeQuery()
.toString();
}
const navItems = [
{
label: 'Articles',
url: '/ceo/content',
icon: <FontIcon className='fa fa-pencil-square-o'></FontIcon>
},
{
label: 'Media',
url: '/ceo/media',
icon: <FontIcon className='mui-icons'>photo</FontIcon>
},
{
label: 'Blog Posts',
url: '/ceo/post',
icon: <FontIcon className='mui-icons'>question_answer</FontIcon>
},
{
label: 'Pages',
url: '/ceo/page',
icon: <FontIcon className='mui-icons'>description</FontIcon>
},
{
label: 'Sections',
url: '/ceo/container',
icon: <FontIcon className='mui-icons'>view_module</FontIcon>
},
{
label: 'Custom Content',
url: '/ceo/custom',
icon: <IconQuilt />
},
{
label: 'Planner',
url: '/ceo/assignment',
icon: <IconEvent />
},
{
label: 'Search',
url: searchUrl, // search remembers the last URL so the state matches when you load it up again
icon: <FontIcon className='mui-icons'>search</FontIcon>
},
'divider'
];
const menuItems = navItems.map((item, i) => {
if (item === 'divider') {
return <Divider key={i} />;
}
return (
<MenuItem
key={i}
onClick={this.navigateTo.bind(this, item.url)}
leftIcon={item.icon}
>
{item.label}
</MenuItem>
);
});
return (
<MuiThemeProvider muiTheme={getMuiTheme(ceoTheme)}>
<div onDragEnd={this.handleInternalEnd.bind(this)} onDragStart={this.handleInternalStart.bind(this)} onDragEnter={this.handleDragEnter}>
{dropzone}
{
this.props.uploadCart.items.size
? <UploadCart />
: ''
}
<Paper
style={{
width:'60px',
overflow:'hidden',
position:'fixed',
top:'0px',
left:'0px',
bottom:'0px',
zIndex:'1100'
}}
rounded={false}
>
<AppBar onClick={this.toggleDrawer.bind(this)} zDepth={0} />
<MenuItem
onClick={this.togglePopover.bind(this, 'firstPopover')}
leftIcon={<IconAdd />}
>
New
</MenuItem>
{menuItems}
<MenuItem
onClick={this.togglePopover.bind(this, 'pluginPopover')}
leftIcon={<FontIcon className='fa fa-plug'></FontIcon>}
>
Plugins
</MenuItem>
<MenuItem
onClick={this.togglePopover.bind(this, 'historyPopover')}
leftIcon={<IconHistory />}
>
History
</MenuItem>
<MenuItem
onClick={this.togglePopover.bind(this, 'secondPopover')}
leftIcon={<FontIcon className='fa fa-cog'></FontIcon>}
>
Settings
</MenuItem>
</Paper>
<Drawer open={this.state.drawerIsOpen} docked={false}>
<AppBar onClick={this.toggleDrawer.bind(this)} />
<MenuItem
onClick={this.togglePopover.bind(this, 'firstPopover')}
leftIcon={<IconAdd />}
>
New
</MenuItem>
{menuItems}
<MenuItem
onClick={this.togglePopover.bind(this, 'pluginPopover')}
leftIcon={<FontIcon className='fa fa-plug'></FontIcon>}
>
Plugins
</MenuItem>
<MenuItem
onClick={this.togglePopover.bind(this, 'historyPopover')}
leftIcon={<IconHistory />}
>
History
</MenuItem>
<MenuItem
onClick={this.togglePopover.bind(this, 'secondPopover')}
leftIcon={<FontIcon className='fa fa-cog'></FontIcon>}
>
Settings
</MenuItem>
</Drawer>
<div className='main-container' style={{'marginLeft': 60}}>
<CeoAppBar
onRequestPasswordChange={this.handleRequestPasswordChange.bind(this)}
/>
<ErrorViewer />
<div className='main-container-inner'>
<TopLevelErrorBoundary>
{this.props.children}
</TopLevelErrorBoundary>
</div>
</div>
<Popover
open={this.state.firstPopover}
anchorEl={this.state.popoverAnchor}
anchorOrigin={{horizontal: 'right', vertical: 'top'}}
targetOrigin={{horizontal: 'left', vertical: 'top'}}
onRequestClose={this.togglePopover.bind(this, 'firstPopover')}
>
<Menu desktop={true}>
<MenuItem primaryText="Add new" disabled={true} style={{fontSize:"0.8em"}} />
<MenuItem primaryText="Article" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/redirect?next=content/new'/>} leftIcon={<FontIcon className='fa fa-pencil-square-o'></FontIcon>} />
<MenuItem primaryText="Blog Post" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/redirect?next=content/new?type=post'/>} leftIcon={<FontIcon className='mui-icons'>question_answer</FontIcon>} />
<MenuItem primaryText="Gallery" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/container/new?type=gallery'/>} leftIcon={<FontIcon className='mui-icons'>photo</FontIcon>} />
<MenuItem primaryText="Page" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/redirect?next=content/new?type=page'/>} leftIcon={<FontIcon className='mui-icons'>description</FontIcon>} />
<MenuItem primaryText="Entry" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/redirect?next=custom/entry'/>} leftIcon={<FontIcon className='mui-icons'>view_quilt</FontIcon>} />
<MenuItem primaryText="Section" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/container/new?omatic'/>} leftIcon={<FontIcon className='mui-icons'>view_module</FontIcon>} />
<MenuItem primaryText="Blog" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/container/new?type=blog'/>} leftIcon={<FontIcon className='mui-icons'>local_activity</FontIcon>} />
<MenuItem primaryText="Planning Assignment" onClick={this.togglePopover.bind(this, 'firstPopover')} containerElement={<Link to='/ceo/assignment/new' />} leftIcon={<FontIcon className='mui-icons'>event_note</FontIcon>} />
<MenuItem primaryText="Upload" onClick={this.handleRequestUpload.bind(this)} leftIcon={<FontIcon className='mui-icons'>file_upload</FontIcon>} />
</Menu>
</Popover>
<Popover
open={this.state.pluginPopover}
anchorEl={this.state.popoverAnchor}
anchorOrigin={{horizontal: 'right', vertical: 'top'}}
targetOrigin={{horizontal: 'left', vertical: 'top'}}
onRequestClose={this.togglePopover.bind(this, 'pluginPopover')}
>
<Menu desktop={true}>
{!this.props.application.plugins.filter(plugin => plugin.get('installed')).size ? <MenuItem disabled={true} primaryText='No plugins installed' /> : ''}
{this.props.application.plugins.filter(plugin => plugin.get('installed')).map(plugin => (
<MenuItem primaryText={plugin.get('name')} onClick={this.togglePopover.bind(this, 'pluginPopover')} containerElement={<Link to={'/ceo/redirect?next=plugins/' + plugin.get('slug')}/>} />
))}
</Menu>
</Popover>
<Popover
open={this.state.historyPopover}
anchorEl={this.state.popoverAnchor}
anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
targetOrigin={{horizontal: 'left', vertical: 'bottom'}}
onRequestClose={this.togglePopover.bind(this, 'historyPopover')}
>
<Menu desktop={true}>
<MenuItem primaryText="Recently viewed" disabled={true} style={{fontSize:"0.8em"}} />
{!this.props.globalHistory.items.size ? <MenuItem primaryText="No open content" disabled={true} /> : ''}
{this.props.globalHistory.items.take(5).map((item, i) => {
return (
<MenuItem
primaryText={truncate(item.get('label'))}
key={i}
leftIcon={this.getIcon(item.get('contentType'))}
onClick={this.handleHistoryLink.bind(this, item)}
/>
);
})}
<Divider />
<MenuItem primaryText="Pending changes" disabled={true} style={{fontSize:"0.8em"}} />
{!this.props.globalHistory.items.filter((item) => item.get('isDirty') === true).size ? <MenuItem primaryText="No pending changes" disabled={true} /> : ''}
{this.props.globalHistory.items.filter((item) => item.get('isDirty') === true).map((item, i) => {
return (
<MenuItem
leftIcon={this.getIcon(item.get('contentType'))}
key={item.get('uuid')}
onClick={this.handleHistoryLink.bind(this, item)}
primaryText={truncate(item.get('label'))}
/>
)
})}
</Menu>
</Popover>
<Popover
open={this.state.secondPopover}
anchorEl={this.state.popoverAnchor}
anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
targetOrigin={{horizontal: 'left', vertical: 'bottom'}}
onRequestClose={this.togglePopover.bind(this, 'secondPopover')}
>
<Menu desktop={true}>
<MenuItem primaryText="Settings" disabled={true} style={{fontSize:"0.8em"}} />
<MenuItem primaryText="About" onClick={this.togglePopover.bind(this, 'secondPopover')} containerElement={<Link to='/ceo/settings/about' />} leftIcon={<FontIcon className='mui-icons'>lightbulb_outline</FontIcon>} />
<MenuItem primaryText="Authors" onClick={this.togglePopover.bind(this, 'secondPopover')} containerElement={<Link to='/ceo/settings/author' />} leftIcon={<FontIcon className='mui-icons'>group</FontIcon>} />
<MenuItem primaryText="Tags" onClick={this.togglePopover.bind(this, 'secondPopover')} containerElement={<Link to='/ceo/settings/tag' />} leftIcon={<FontIcon className='fa fa-tag'></FontIcon>} />
<MenuItem primaryText="Issues" onClick={this.togglePopover.bind(this, 'secondPopover')} containerElement={<Link to='/ceo/settings/issue' />} leftIcon={<FontIcon className='mui-icons'>view_column</FontIcon>} />
<MenuItem primaryText="Developer Access" onClick={this.togglePopover.bind(this, 'secondPopover')} containerElement={<Link to='/ceo/developer' />} leftIcon={<FontIcon className='mui-icons'>code</FontIcon>} />
<MenuItem primaryText="Site Settings" onClick={this.togglePopover.bind(this, 'secondPopover')} containerElement={<Link to='/ceo/site' />} leftIcon={<FontIcon className='mui-icons'>public</FontIcon>} />
<MenuItem primaryText="Admin" onClick={this.togglePopover.bind(this, 'secondPopover')} containerElement={<Link to='/ceo/admin' />} leftIcon={<FontIcon className='fa fa-lock'></FontIcon>} />
</Menu>
</Popover>
<RootExtension />
<footer>
<div className='row between-xs'>
<div className='col-xs-3'>
<div className="box">
<FlatButton
label='Service Status'
href='http://uptime.getsnworks.com'
style={{margin: 10, color: '#333'}}
icon={<FontIcon className="mui-icons">cloud_done</FontIcon>}
/>
</div>
</div>
<div className='col-xs-3 middle-xs'>
<div className="box">
<Quips randomize={50000} />
</div>
</div>
<div className='col-xs-3'>
<div className="box">
<ul>
<li>Build {Config.APPID} {Config.get('system_version')}</li>
{Config.get('debug') ? <li>Reporting React {Config.REACT}; Phalcon {Config.PHALCON}</li> : '' }
{
this.props.applicationState.connectedState
? (
<li><span className='mui-icons'>cloud_queue</span> Connected</li>
)
: (
<li><span className='mui-icons'>cloud_off</span> Offline</li>
)
}
</ul>
</div>
</div>
</div>
<div className='row middle-xs center-xs'>
<div className='col-xs-12'>
<small className='quiet'>
© State News, Inc | Made with love in the Awesome/Angry Mitten
</small>
</div>
</div>
</footer>
<NotificationBar />
<SystemAlerts />
<ChangeLog
onRequestClose={this.handleChangelogClose.bind(this)}
/>
<UserChangePassword
open={this.state.userPasswordModalOpen}
onRequestClose={this.handleRequestPasswordChangeClose.bind(this)}
onUpdatePassword={this.handleUpdatePassword.bind(this)}
errorMessage={this.state.userPasswordError}
/>
</div>
</MuiThemeProvider>
);
}
}
const mapStateToProps = (state) => {
return {
globalSearch: state.globalSearch,
globalHistory: state.globalHistory,
applicationState: state.application,
uploadCart: state.uploadCart,
application: state.application
};
};
export default connect(mapStateToProps)(App);