Home Reference Source

application/components/container/container-item.js

import React from 'react';
import {connect} from 'react-redux';
import _ from 'lodash';

import ceoTheme from './../../theme';

import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem';
import TextField from 'material-ui/TextField';
import FontIcon from 'material-ui/FontIcon';
import Paper from 'material-ui/Paper';
import {List, ListItem} from 'material-ui/List';
import Subheader from 'material-ui/Subheader';
import Divider from 'material-ui/Divider';

import {browserHistory, Link} from 'react-router';
import Config from './../../config';

import Immutable from 'immutable';
import moment from 'moment';
import ContentMetaProperties from './../content/content-meta-properties';

import CurrentUser from './../../current-user';
import {sluggify} from './../../util/strings';

import SimplePublishModal from './../content/content-simple-publish';

import {
    containersMaybeFetch,
    containersFetch,
    containerCreate,
    containerUpdate,
    containersRemove,
    containersCheckIn,
    containersCheckOut,
    containerFetchOne
} from './../../redux/actions/container-actions';

import {
    snackbarShowMessage
} from './../../redux/actions/snackbar-actions';

import {
    globalHistoryPush,
    globalHistoryPop
} from './../../redux/actions/global-history-actions';

import {timeToFormat} from './../../util/strings';

import LoadingIndicator from './../common/loading-indicator';
import ContentSearchModal from './../common/content-search-modal';
import RichEditor from './../common/rich-editor';
import TagSearchBox from './../common/tag-search-box';

import {rawToHtml} from './../../util/rawToHtml';

import {Row, Col} from './../flexbox';
import LockButtons from './../lock-buttons';
import DeleteButton from './../common/delete-button';
import ContainerSortableContent from './container-sortable-content';

class ContainerItem extends React.Component {

    constructor(props) {
        super(props);

        this.getUuid = this.getUuid.bind(this);
        this.isNew = this.isNew.bind(this);
        this.isUserEditable = this.isUserEditable.bind(this);

        this.handleFormChange = this.handleFormChange.bind(this);
        this.handleEditorChange = this.handleEditorChange.bind(this);
        this.handleMetaFormChange = this.handleMetaFormChange.bind(this);

        this.getRouter = this.getRouter.bind(this);

        this.freezeContainer = this.freezeContainer.bind(this);
        this.thawContainer = this.thawContainer.bind(this);

        this.state = {
            contentIsOpen: false,
            dirty: false,
            didDelete: false,
            edited: {
                tags: [],
                content: []
            }
        }
    }

    isUserEditable() {
        if (this.isNew()) {
            return true;
        }

        if (!this.state.edited.lock) {
            return false;
        }

        if (this.state.edited.lock.user_id === CurrentUser.getId()) {
            return true;
        }

        return false;
    }

    getUuid() {
        if (this.props.uuid) {
            return this.props.uuid;
        }

        if (this.props.params && this.props.params.id) {
            return this.props.params.id;
        }

        return false;
    }

    isNew() {
        if (!this.getUuid() || this.getUuid() === 'new') {
            return true;
        }

        return false;
    }

    getRouter() {
        return this.props.router;
    }

    componentWillMount() {

        const {dispatch} = this.props;

        if (!this.isNew()) {
            dispatch(containersMaybeFetch())
                .then(() => dispatch(containerFetchOne(this.getUuid())))
                .then(() => {
                    let container = this.props.containers.items.find((item) => item.get('uuid') == this.getUuid());

                    let dirty = false;

                    // pause here for a second and see if we have a modified version in the global history state
                    const savedState = this.props.globalHistory.items.find((item) => item.get('uuid') == this.getUuid());

                    if (savedState && savedState.size && savedState.get('isDirty')) {
                        // we have a freeze dried state. reconstitute that
                        // then set the dirty state to 'true'
                        container = this.thawContainer(savedState.get('cachedObject'));
                        dirty = true;
                    }

                    this.setState({'edited': container.toJS(), 'dirty': dirty});
                });
        } else {
            dispatch(containersMaybeFetch());

            this.setState({'edited': {
                tags: [],
                content: [],
                dirty: false,
                type: this.props.router.location.query.type ? this.props.router.location.query.type : false
            }});
        }
        this.setState({'publishIsOpen': false});
    }

    componentDidMount() {
        this.getRouter().setRouteLeaveHook(this.props.route, this.routerWillLeave.bind(this));
    }

    routerWillLeave(nextLocation) {
        if (!this.isNew() && !this.state.didDelete) {
            const {dispatch} = this.props;
            dispatch(globalHistoryPush({
                'label': this.state.edited.title ? this.state.edited.title : this.state.edited.slug,
                'type': 'container',
                'contentType': 'container',
                'url': '/ceo/container/' + this.state.edited.uuid,
                'uuid': this.state.edited.uuid,
                'isDirty': this.state.dirty,
                'cachedObject': (this.state.dirty ? this.freezeContainer(this.state.edited) : {})
            }));
        }

        if (this.isNew() && this.isUserEditable() && this.state.dirty && this.state.dirty === true) {
            return 'You have not saved this container! Any unsaved changes will be lost once you click OK. Click cancel and then save to preserve your work. Are you still sure you want to leave this item?';
        }
    }

    freezeContainer(container) {
        let toFreeze = container;

        let rawContent = this.state.editorContent ? this.state.editorContent : toFreeze.description;
        let rawState = this.state.editorRaw ? this.state.editorRaw : toFreeze.description_raw;
        // console.log(rawContent);
        toFreeze.description = rawContent;
        toFreeze.description_raw = rawState;

        return toFreeze;
    }

    thawContainer(container) {
        let toThaw = container;

        this.state.editorContent = toThaw.get('description') ? toThaw.get('description') : '';
        this.state.editorRaw = toThaw.get('description_raw') ? toThaw.get('description_raw') : '';

        return toThaw;
    }

    // callbacks to handle the tag selection
    onSelectTag(tag) {
        let container = this.state.edited;
        const existing = _.find(container.tags, {'uuid': tag.get('uuid')});
        if (!existing) {
            container.tags.push(tag.toJS());
            this.setState({'edited': container, 'dirty': true});
        }
    }

    handleMetaFormChange(fieldData) {
        if (fieldData) {
            let container = Immutable.fromJS(this.state.edited);
            let meta = container.get('meta');

            if (!meta) {
                meta = Immutable.fromJS({});
            }
            meta = meta.set(fieldData.field, Immutable.fromJS(fieldData));

            container = container.set('meta', meta);
            this.setState({'edited': container.toJS(), 'dirty': true});
        }
    }

    onRemoveTag(tag) {
        let container = this.state.edited;
        container.tags = container.tags.filter((item, i) => {
            return tag.uuid != item.uuid;
        });

        this.setState({'edited': container, 'dirty': true});
    }

    // state management
    handleSave(andCheckin = false) {
        const {dispatch} = this.props;

        let data = {
            'title':                this.state.edited.title,
            'slug':                 this.state.edited.slug,
            'description':          this.state.editorContent,
            'description_raw':      this.state.editorRaw,
            'type':                 this.state.edited.type,
            'layout_template':      this.state.edited.layout_template,
            'published_at':         this.state.edited.published_at
        };

        if (this.state.newSortOrder) {
            data['sort_order'] = this.state.newSortOrder;
        }

        if (this.state.edited.content && this.state.edited.content.length) {
            data['content'] = this.state.edited.content.map((content, i) => {
                return content.uuid;
            })
        } else {
            data['content'] = [];
            data['sort_order'] = [];
        }

        if (this.state.edited.tags && this.state.edited.tags.length) {
            data['tags'] = this.state.edited.tags.map((tag, i) => {
                return tag.uuid;
            });
        } else {
            data['tags'] = [];
        }

        if (this.state.edited.meta) {
            data['meta'] = this.state.edited.meta;
        }

        dispatch(snackbarShowMessage('Saving...', false));

        if (!this.isNew()) {
            // Update existing is pretty straightforward
            // update via the uuid
            dispatch(containerUpdate(this.getUuid(), data))
                .then(() => {
                    // reset the edited object to the new item
                    const container = this.props.containers.items.find((item) => item.get('uuid') == this.state.edited.uuid);
                    this.setState({'edited': container.toJS(), 'dirty': false});
                })
                .then(() => {
                    // flash the message
                    dispatch(snackbarShowMessage('Container updated'));
                })
                .then(() => {
                    if (andCheckin !== true) {
                        return;
                    }

                    this.handleCheckin();
                })
                .then(() => dispatch(containerFetchOne(this.getUuid())));
        } else {
            // Create is slightly different
            // first, create it
            dispatch(containerCreate(data))
                .then(() => {
                    // then grab the newly created item and pass it on
                    const container = this.props.containers.items.find((item) => item.get('uuid') == this.props.containers.selected);
                    dispatch(snackbarShowMessage('Container created'));
                    return container;
                })
                .then((container) => {
                    // update the state with the newly created object
                    this.setState({
                        'edited': container.toJS(),
                        'dirty': false
                    }, () => {
                        browserHistory.push('/ceo/container/' + this.props.containers.selected);
                    });
                });
        }
    }

    handleFormChange(e) {
        const val = e.target.value;
        const key = e.target.getAttribute('for');

        let container = this.state.edited;

        container[key] = val;

        if (key === 'slug') {
            container[key] = sluggify(val, true);
        }

        this.setState({'edited': container, 'dirty': true});
    }

    handleSelectChange(id, e, index, val) {
        let container = this.state.edited;

        container[id] = val;
        this.setState({'edited': container, 'dirty': true});
    }

    handleEditorChange(raw, editorState, name) {
        this.setState({
            'editorRaw': raw,
            'editorContent': this.toHtml(editorState.getCurrentContent()),
            'dirty': true
        });
    }

    toHtml(state) {
        return rawToHtml(state);
    }

    handleSort(sorted) {
        this.setState({'newSortOrder': sorted, 'dirty': true});
    }

    handleSelectContent(content) {
        let container = this.state.edited;
        const existing = _.find(container.content, {'uuid': content.get('uuid')});
        const {dispatch} = this.props;

        if (this.state.edited.type == 'gallery' && content.get('attachment').get('type') == 'gallery') {
            dispatch(snackbarShowMessage("Oops, I can't add a gallery to a gallery"));
            return;
        }

        if (!existing) {
            dispatch(snackbarShowMessage('Content added'));
            container.content.push(content.toJS());

            if (container.sort_order) {
                container.sort_order.push(content.get('uuid'));
            }

            this.setState({'edited': container, 'dirty': true});
        }
    }

    handleRemoveContent(content, order) {
        let container = this.state.edited;
        const {dispatch} = this.props;
        container.content = container.content.filter((item, i) => {
            return content.uuid != item.uuid;
        });
        if (container.sort_order && order) {
            container.sort_order = order

        }

        dispatch(snackbarShowMessage('Content removed'));
        this.setState({'edited': container, 'dirty': true});
    }

    handleOpenContent() {
        this.setState({'contentIsOpen': true});
    }

    handleCloseContent() {
        this.setState({'contentIsOpen': false});
    }

    handleCheckout(e) {
        const {dispatch} = this.props;
        return dispatch(containersCheckOut(this.state.edited.srn))
            .then(() => dispatch(containerFetchOne(this.state.edited.uuid)))
            .then(() => {
                const container = this.props.containers.items.find((item) => item.get('uuid') == this.state.edited.uuid);
                this.setState({'edited': container.toJS(), 'dirty': true});
            });
    }

    handleCheckin(e) {
        const {dispatch} = this.props;
        dispatch(containersCheckIn(this.getUuid(), this.state.edited.lock.uuid))
            .then(() => dispatch(containerFetchOne(this.getUuid())))
            .then(() => {
                const container = this.props.containers.items.find((item) => item.get('uuid') == this.state.edited.uuid);
                this.setState({'edited': container.toJS(), 'dirty': true});
            });
    }

    handleSaveCheckIn(e) {
        this.handleSave(true);
    }

    handleDelete(e) {
        const {dispatch} = this.props;
        dispatch(containersRemove(this.getUuid()))
            .then(() => dispatch(globalHistoryPop(this.getUuid())))
            .then(() => this.setState({'didDelete': true}, () => {
                browserHistory.push('/ceo/container');
                dispatch(snackbarShowMessage('Container removed'));
            }));
    }

    handleOpenPublish() {
        this.setState({'publishIsOpen': true});
    }

    handleClosePublish() {
        this.setState({'publishIsOpen': false});
    }

    onPublish(pubData) {
        let container = this.state.edited;

        container['published_at'] = pubData.published_at;
        this.setState({'edited': container, 'dirty': true}, () => {
            this.handleSave();
        });
    }

    handleCancelPublish() {
        let container = this.state.edited;

        container['published_at'] = null;
        this.setState({'edited': container, 'dirty': true}, () => {
            this.handleSave();
        });
    }

    handlePreview() {
        const url = '/content/preview/' + this.state.edited.uuid;

        window.open(url, 'preview');
    }

    render() {
        if (!this.isNew() && !this.state.edited.uuid) {
            return (
                <div className="content-item-root">
                    <Row>
                        <Col xs={12}>
                            <Paper className='padded clear-top clear-bottom'>
                                <LoadingIndicator />
                            </Paper>
                        </Col>
                    </Row>
                </div>
            );
        }

        const container_types = ['automatic', 'manual', 'gallery', 'blog'];

        let sort_order = [];
        if (this.state.edited.sort_order && this.state.edited.sort_order.length) {
            sort_order = this.state.edited.sort_order;
        } else if (this.state.edited.content.length) {
            sort_order = this.state.edited.content.map((item) => item.uuid);
        }

        let layout_templates = [];
        if (Config && Config.get('layout_templates')) {
            const all_layout_templates = Config.get('layout_templates');
            layout_templates = all_layout_templates['container'] ? all_layout_templates['container'] : [];
        }

        return (
            <div className='container-item-root'>
                <Row>
                    <Col xs={12}>
                        <Paper className='toolbar'>
                            <Row middle='xs'>
                                <Col xs={6}>
                                    <LockButtons
                                        onSave={this.handleSave.bind(this)}
                                        onSaveCheckIn={this.handleSaveCheckIn.bind(this)}
                                        onCheckIn={this.handleCheckin.bind(this)}
                                        onCheckOut={this.handleCheckout.bind(this)}
                                        userEditable={this.isUserEditable()}
                                        lockable={this.state.edited}
                                        />
                                </Col>
                                <Col xs={6} end='xs'>
                                    <FlatButton
                                        className='action-preview'
                                        onClick={this.handlePreview.bind(this)}
                                        disabled={this.isNew() ? true : false}
                                        label="Preview"
                                        icon={<FontIcon className='mui-icons'>desktop_mac</FontIcon>}
                                        />
                                </Col>
                            </Row>
                        </Paper>

                        <Row>
                            <Col xs={10}>
                                <Paper className='padded'>
                                    <Row>
                                        <Col xs={6}>
                                            <SelectField
                                                disabled={this.isUserEditable() ? false : true}
                                                fullWidth={true}
                                                floatingLabelText='Container Type'
                                                floatingLabelFixed={true}
                                                errorText={'See an explanation of container types on the right'}
                                                errorStyle={{color: ceoTheme.palette.disabledColor}}
                                                onChange={this.handleSelectChange.bind(this, 'type')}
                                                value={this.state.edited.type ? this.state.edited.type : ''}
                                                >
                                                {container_types.map((type, i) => {
                                                    return <MenuItem key={i} value={type} primaryText={type} />;
                                                })}
                                            </SelectField>
                                        </Col>
                                        <Col xs={6}>
                                            <SelectField
                                                disabled={this.isUserEditable() ? false : true}
                                                fullWidth={true}
                                                floatingLabelText='Layout Template'
                                                onChange={this.handleSelectChange.bind(this, 'layout_template')}
                                                value={this.state.edited.layout_template ? this.state.edited.layout_template : false}
                                                >
                                                <MenuItem value={false} primaryText='None' />
                                                {layout_templates.map((item, i) => {
                                                    return <MenuItem key={i} value={item} primaryText={item} />;
                                                })}
                                            </SelectField>
                                        </Col>
                                    </Row>
                                    <TextField
                                        disabled={this.isUserEditable() ? false : true}
                                        htmlFor='slug'
                                        fullWidth={true}
                                        floatingLabelText='Slug'
                                        onChange={this.handleFormChange}
                                        value={this.state.edited.slug ? this.state.edited.slug : ''}
                                        ref='slugTextField'
                                        />
                                    <TextField
                                        disabled={this.isUserEditable() ? false : true}
                                        htmlFor='title'
                                        fullWidth={true}
                                        floatingLabelText='Title'
                                        onChange={this.handleFormChange}
                                        value={this.state.edited.title ? this.state.edited.title : ''}
                                        />

                                    {
                                        this.state.edited.type === 'automatic' || this.state.edited.type === 'gallery'
                                        ? (
                                            <Row middle='xs' flexy={true}>
                                                <Col flex={0} style={{'paddingRight': this.state.edited.tags.length ? '10px' : 0}}>
                                                    {this.state.edited.tags.map((tag, i) => {
                                                        return (
                                                            <div
                                                                style={{'marginTop': '25px'}}
                                                                className='pill'
                                                                key={i}
                                                                >
                                                                {tag.name}
                                                                <FontIcon
                                                                    className='mui-icons'
                                                                    onClick={this.onRemoveTag.bind(this, tag)}
                                                                    >close</FontIcon>
                                                            </div>
                                                        );
                                                    })}
                                                </Col>
                                                <Col flex={5}>
                                                    <TagSearchBox
                                                        disabled={this.isUserEditable() ? false : true}
                                                        onSelectTag={this.onSelectTag.bind(this)}
                                                        />
                                                </Col>
                                            </Row>

                                        )
                                        :''

                                    }

                                </Paper>

                                <Paper className='padded clear-top'>
                                    <RichEditor
                                        label='Description'
                                        defaultValue={this.state.edited.description_raw ? this.state.edited.description_raw : this.state.edited.description}
                                        onChange={this.handleEditorChange} />
                                </Paper>

                                {
                                    this.state.edited.type == 'manual' || this.state.edited.type == 'gallery'
                                    ? (
                                        <Paper className='padded clear-top clear-bottom'>
                                            <label className='fixed-label'>Linked Content</label>
                                            <ContainerSortableContent
                                                container={this.state.edited}
                                                order={sort_order}
                                                onRemove={this.handleRemoveContent.bind(this)}
                                                onOpen={this.handleOpenContent.bind(this)}
                                                onSort={this.handleSort.bind(this)}
                                                disabled={this.isUserEditable() ? false : true}
                                                />
                                            <Row>
                                                <Col xs={12}>
                                                    <RaisedButton
                                                        onClick={this.handleOpenContent.bind(this)}
                                                        secondary={true}
                                                        label='Attach Content'
                                                        style={{marginRight:'10px'}}
                                                        disabled={this.isUserEditable() ? false : true}
                                                        />
                                                    {
                                                        this.state.edited.type != 'gallery'
                                                        ? (
                                                            <RaisedButton
                                                                containerElement={<Link to={'/ceo/content/new?container='+this.state.edited.uuid} />}
                                                                label='Create Content'
                                                                disabled={this.isUserEditable() ? false : true}
                                                                />
                                                        )
                                                        : ''
                                                    }
                                                </Col>
                                            </Row>
                                        </Paper>
                                    )
                                    : ''
                                }

                                <Paper className='clear-top padded'>
                                    <Subheader>Meta Properties</Subheader>

                                    <ContentMetaProperties
                                        group='container'
                                        disabled={this.isUserEditable()}
                                        onChange={this.handleMetaFormChange.bind(this)}
                                        content={Immutable.fromJS(this.state.edited)}
                                        />
                                </Paper>

                            </Col>
                            <Col xs={2}>
                                <Paper className='padded clear-bottom'>
                                    <p className='small'>
                                        <strong>Automatic</strong> containers use tags to associate content, then weight and publication date to order them.
                                    </p>
                                    <p className='small'>
                                        <strong>Manual</strong> containers have manually managed and ordered content.
                                    </p>
                                    <p className='small'>
                                        <strong>Gallery</strong> containers are manual containers for media files.
                                    </p>
                                    <p className='small'>
                                        <strong>Blog</strong> containers are special groups for blog posts, and cannot be ordered.
                                    </p>
                                </Paper>

                                <Paper className='clear-bottom'>
                                    <List>
                                        <Subheader>Published</Subheader>
                                        {
                                            this.state.edited.published_at
                                            ? (
                                                <ListItem
                                                    disabled={this.isUserEditable() ? false : true}
                                                    primaryText={this.state.edited.published_at ? timeToFormat(this.state.edited.published_at, 'LT l') : 'Not published'}
                                                    leftIcon={<FontIcon className='mui-icons'>check</FontIcon>}
                                                    />
                                            )
                                            : (
                                                <ListItem
                                                    disabled={this.isUserEditable() ? false : true}
                                                    primaryText='Not Published'
                                                    leftIcon={<FontIcon className='mui-icons'>error_outline</FontIcon>}
                                                    />
                                            )
                                        }

                                        {
                                            this.state.edited.published_at
                                            ? (
                                                <div className='center-align'>
                                                    <RaisedButton
                                                        style={{width:'90%'}}
                                                        className='secondary-button'
                                                        onClick={this.handleCancelPublish.bind(this)}
                                                        disabled={this.isUserEditable() ? false : true}
                                                        label="Unpublish"
                                                        icon={<FontIcon className='mui-icons'>cloud_download</FontIcon>}
                                                        />
                                                </div>
                                            )
                                            : (
                                                <div className='center-align'>
                                                    <RaisedButton
                                                        style={{width:'90%'}}
                                                        className='secondary-button'
                                                        onClick={this.handleOpenPublish.bind(this)}
                                                        disabled={this.isUserEditable() ? false : true}
                                                        label="Publish"
                                                        icon={<FontIcon className='mui-icons'>cloud_upload</FontIcon>}
                                                        />
                                                </div>
                                            )
                                        }
                                    </List>
                                </Paper>
                                <div className='clear-bottom'>
                                    <DeleteButton
                                        className='full'
                                        fullWidth={true}
                                        icon={true}
                                        disabled={this.isNew() || !this.isUserEditable() ? true : false}
                                        onDelete={this.handleDelete.bind(this)}
                                        />
                                </div>
                            </Col>
                        </Row>

                        <ContentSearchModal
                            isOpen={this.state.contentIsOpen}
                            contentType={this.state.edited.type == 'gallery' ? 'media' : false}
                            onRequestClose={this.handleCloseContent.bind(this)}
                            onSelectContent={this.handleSelectContent.bind(this)}
                            />

                        {
                            this.state.publishIsOpen
                            ? (
                                <SimplePublishModal
                                    content={this.state.edited}
                                    isOpen={this.state.publishIsOpen}
                                    onPublish={this.onPublish.bind(this)}
                                    onClose={this.handleClosePublish.bind(this)}
                                    />
                            )
                            : ''
                        }
                    </Col>
                </Row>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    return {
        containers: state.containers,
        globalHistory: state.globalHistory
    }
}

export default connect(mapStateToProps)(ContainerItem);