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);