Home Reference Source

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'>
                                    &copy; 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);