application/components/content/content-item.js
import React from 'react';
import Immutable from 'immutable';
import _ from 'lodash';
import {Entity} from 'draft-js';
import {browserHistory, Link} from 'react-router';
import BaseView from './../base-view';
import {connect} from 'react-redux';
import moment from 'moment';
import {withDraftable} from './../../hocs/draftable';
import ceoTheme from './../../theme';
import RaisedButton from 'material-ui/RaisedButton';
import FlatButton from 'material-ui/FlatButton';
import IconButton from 'material-ui/IconButton';
import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem';
import TextField from 'material-ui/TextField';
import Checkbox from 'material-ui/Checkbox';
import Toggle from 'material-ui/Toggle';
import FontIcon from 'material-ui/FontIcon';
import Paper from 'material-ui/Paper';
import Divider from 'material-ui/Divider';
import CircularProgress from 'material-ui/CircularProgress';
import {List, ListItem} from 'material-ui/List';
import {GridList, GridTile} from 'material-ui/GridList';
import Subheader from 'material-ui/Subheader';
import Avatar from 'material-ui/Avatar';
import {Row, Col} from './../flexbox';
import {stateToHTML} from 'draft-js-export-html';
// import stateToHTML from './../../exportToHTML_modified';
import {error, info, warning} from './../../util/console';
import {sluggify, timeToFormat} from './../../util/strings';
import Config from './../../config';
import CurrentUser from './../../current-user';
import LoadingIndicator from './../common/loading-indicator';
import RichEditor from './../common/rich-editor';
import RichEditor2 from './../common/rich-editor2';
import DraftSerializer from '../common/rich-editor2/util/draft-serializer';
import {ALIGNMENT_DATA_KEY} from './../common/rich-editor/libs/extend-rich-utils';
import SimpleEditor from './../common/simple-editor';
import UserSearchBox from './../common/user-search-box';
import DraftLastAutosave from './../draftable/draft-last-autosave';
import DraftRestore from './../draftable/draft-restore';
import ContentAttachmentView from './content-attachment-view';
import ContentAttachmentPreview from './content-attachment-preview';
import ContentMetaProperties from './content-meta-properties';
import ContentLock from './content-lock';
import PublishModal from './content-publish';
import SimplePublishModal from './content-simple-publish';
import DiffModal from './content-diff';
import ExportModal from './content-export';
import DominantMediaModal from './content-dom-media';
import ContentAssignment from './content-assignment';
import AuthorSearchBox from './../common/author-search-box';
import TagSearchBox from './../common/tag-search-box';
import SstsSearchBox from './../common/ssts-search-box';
import NoteViewer from './../common/rich-editor/note-viewer';
import DeleteButton from './../common/delete-button';
import CharacterCounter from './../common/character-counter';
import WordCounter from './../common/word-counter';
import UrlHandler from './url-handler';
import {rawToHtml} from './../../util/rawToHtml';
import {
workflowsMaybeFetch,
workflowsFilterContentOnly,
workflowsFilterPrintOnly
} from './../../redux/actions/workflow-actions';
import {
workflowSectionsMaybeFetch,
workflowSectionsFilterContentOnly,
workflowSectionsFilterPrintOnly
} from './../../redux/actions/workflow-section-actions';
import {
issuesMaybeFetch,
issuesFilterOpen
} from './../../redux/actions/issue-actions';
import {
snackbarShowMessage,
snackbarClose
} from './../../redux/actions/snackbar-actions';
import {
alertShowMessage
} from './../../redux/actions/alert-actions';
import {
globalHistoryPush,
globalHistoryPop
} from './../../redux/actions/global-history-actions';
import {
applicationIsDirty,
applicationFetchSettings
} from './../../redux/actions/application-actions';
import {
contentMaybeFetchCache,
contentFetchOne,
contentCheckIn,
contentCheckOut,
contentRemove,
contentUpdate,
contentCreate,
contentAttachmentUpdate,
contentSyncPrint,
contentClearLock
} from './../../redux/actions/content-actions';
import {
containersFetch
} from './../../redux/actions/container-actions';
class ContentItem extends BaseView {
constructor(props) {
super(props);
this.getUuid = this.getUuid.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleSaveCheckIn = this.handleSaveCheckIn.bind(this);
this.handlePreview = this.handlePreview.bind(this);
this.handleEditorChange = this.handleEditorChange.bind(this);
this.handleEditorChange2 = this.handleEditorChange2.bind(this);
this.handleFormChange = this.handleFormChange.bind(this);
this.handleMetaFormChange = this.handleMetaFormChange.bind(this);
this.handleCheckout = this.handleCheckout.bind(this);
this.handleCheckin = this.handleCheckin.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handleRestoreDraft = this.handleRestoreDraft.bind(this);
this.handleOpenPublish = this.handleOpenPublish.bind(this);
this.handleClosePublish = this.handleClosePublish.bind(this);
this.isUserEditable = this.isUserEditable.bind(this);
this.freezeContent = this.freezeContent.bind(this);
this.thawContent = this.thawContent.bind(this);
this.keyTimer = null;
this.contentData = {};
this.abstractData = {};
this.onChange = this.onChange.bind(this);
this.getRouter = this.getRouter.bind(this);
this.richEditorRefCount = 0;
this.state = {
content: Immutable.fromJS({
authors: [],
tags: []
}),
diffIsOpen: false,
exportIsOpen: false,
domMediaIsOpen: false,
contentLimbo: '',
abstractLimbo: '',
didDelete: false
};
}
onChange(state) {
this.setState(state);
}
getRouter() {
return this.props.router;
}
/**
* Return the requested UID, starts with UUID property,
* then goes to the URL property.
* @return {string}
*/
getUuid() {
if (this.props.uuid) {
return this.props.uuid;
}
if (this.props.params && this.props.params.id) {
return this.props.params.id;
}
return false;
}
toHtml(contentState) {
return rawToHtml(contentState);
}
componentWillUpdate(nextProps, nextState) {
if (nextProps.params.id && nextProps.params.id != this.props.params.id) {
this.componentWillMount.call(this);
}
}
componentWillMount() {
const {dispatch} = this.props;
if (!this.isNew()) {
dispatch(contentFetchOne(this.getUuid()))
.then(() => {
let content = this.props.content.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'
content = this.thawContent(savedState.get('cachedObject'));
dirty = true;
}
this.setState({'content': content, 'dirty': dirty}, () => {
this.props.draftable.doesHaveDraft({'srn': this.state.content.get('srn')}, (draft) => {
if (draft) {
this.setState({'pendingDraft': draft});
}
});
this.props.draftable.setDraftCreatedHook(() => {
this.setState({'lastSaved': moment().utc()});
});
});
});
} else {
this.setState({'content': Immutable.fromJS({
authors: [],
tags: [],
type: this.getRouter().location.query.type ? this.getRouter().location.query.type : 'article'
})});
}
dispatch(workflowsMaybeFetch())
.then(() => dispatch(containersFetch({type: ['blog']})))
.then(() => dispatch(workflowsFilterContentOnly()))
.then(() => dispatch(workflowsFilterPrintOnly()))
.then(() => {
if (this.isNew() && this.getRouter().location.query.type == 'post' && this.props.containers.items) {
this.setState({
'content': this.state.content.set('container_id', this.props.containers.items.first().get('id'))
});
}
});
dispatch(workflowSectionsMaybeFetch());
dispatch(issuesMaybeFetch())
.then(() => dispatch(issuesFilterOpen()));
this.setState({'publishIsOpen': false});
}
componentDidMount() {
if (this.state.content.get('content') && !this.state.contentLimbo) {
this.state.contentLimbo = this.state.content.get('content').replace(/(<([^>]+)>)/ig, "")
}
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.content.get('title') ? this.state.content.get('title') : this.state.content.get('slug'),
'type': 'content',
'contentType': this.state.content.get('type'),
'url': '/ceo/content/' + this.state.content.get('uuid'),
'uuid': this.state.content.get('uuid'),
'isDirty': this.state.dirty,
'cachedObject': (this.state.dirty ? this.freezeContent(this.state.content) : {})
}));
}
if (this.isNew() && this.isUserEditable() && this.state.dirty && this.state.dirty === true) {
return 'You have not saved this item! 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?';
}
}
/**
* Freeze-dry content to be so it can be picked up later
* @param {object} content
*/
freezeContent(content) {
let toFreeze = content.asMutable();
// ok, so here's the thing, the rich editor is _initialized_ via the content_raw property, but
// the actual output is stored in contentData.raw. So we have to sort of manage the state
// for both items when freezing and thawing.
let rawContent = this.contentData.raw ? JSON.stringify(this.contentData.raw) : toFreeze.get('content_raw');
let rawAbstract = this.abstractData.raw ? JSON.stringify(this.abstractData.raw) : toFreeze.get('abstract_raw');
toFreeze.set('content_raw', rawContent);
toFreeze.set('content', this.contentData.html ? this.contentData.html : toFreeze.get('content'));
toFreeze.set('abstract_raw', rawAbstract);
toFreeze.set('abstract', this.abstractData.html ? this.abstractData.html : toFreeze.get('abstract'));
return toFreeze.asImmutable();
}
/**
* Thaw freeze-dried content
* @param {object} content
*/
thawContent(content) {
let toThaw = content.asMutable();
// We need to populate this so the user can save right after thawing a content item and the content block
// saves properly. If we don't set this, then the text content of wouldn't update properly unless the user
// made changes in the editor before saving (forcing the component to update contentData.raw itself).
this.contentData.raw = toThaw.get('content_raw') ? JSON.parse(toThaw.get('content_raw')) : '';
this.contentData.html = toThaw.get('content');
this.abstractData.raw = toThaw.get('abstract_raw') ? JSON.parse(toThaw.get('abstract_raw')) : '';
this.abstractData.html = toThaw.get('abstract');
return toThaw.asImmutable();
}
isNew() {
if (!this.getUuid() || this.getUuid() === 'new') {
return true;
}
return false;
}
isLocked() {
if (this.isNew()) {
return false;
}
if (this.state.content.get('lock')) {
return true;
}
return false;
}
isUserEditable() {
if (this.isNew()) {
return true;
}
if (!this.state.content.get('lock')) {
return false;
}
if (this.state.content.get('lock').get('user_id') === CurrentUser.getId()) {
return true;
}
return false;
}
handleCheckout(e) {
const {dispatch} = this.props;
return dispatch(contentCheckOut(this.state.content.get('srn')))
.then(() => dispatch(contentFetchOne(this.state.content.get('uuid'))))
.then(() => {
const content = this.props.content.items.find((item) => item.get('uuid') == this.state.content.get('uuid'));
this.contentData = {};
this.abstractData = {};
this.setState({'content': content});
});
}
handleCheckin(e) {
const {dispatch} = this.props;
return dispatch(contentCheckIn(this.getUuid(), this.state.content.get('lock').get('uuid')))
.then(() => dispatch(contentFetchOne(this.getUuid())))
.then(() => {
const content = this.props.content.items.find((item) => item.get('uuid') == this.state.content.get('uuid'));
this.setState({'content': content});
});
}
handleDelete(e) {
const {dispatch} = this.props;
dispatch(contentRemove(this.getUuid()))
.then(() => dispatch(globalHistoryPop(this.getUuid())))
.then(() => this.setState({'didDelete': true}, () => {
browserHistory.push('/ceo/content');
dispatch(snackbarShowMessage('Content removed'));
}))
.catch(() => {
dispatch(snackbarClose());
dispatch(alertShowMessage({
'title': 'Oops',
'message': (
<p>There was a problem removing your content. You may not have permission to delete.</p>
)
}));
this.setState({'dirty': true});
dispatch(applicationIsDirty(true));
});
}
handleRestoreDraft() {
if (!this.state.pendingDraft) {
return;
}
const newContent = this.state.content.set(
'content_raw',
this.state.pendingDraft.get('content_raw')
);
this.setState({'content': newContent, 'pendingDraft': false});
}
handleFormChange(e) {
var prop = e.target.getAttribute('for');
if (prop) {
let newContent;
if (prop === 'slug') {
newContent = this.state.content.set(
'slug', sluggify(e.target.value, true)
);
} else if (prop === 'title') {
newContent = this.state.content
.set('title', e.target.value);
} else if (prop === 'title_url') {
newContent = this.state.content
.set('title_url', sluggify(e.target.value, true));
} else if (prop === 'weight') {
newContent = this.state.content.set(
'weight', parseInt(e.target.value)
);
} else if (prop == 'click_through') {
newContent = this.state.content.set(
'click_through', e.target.value
);
}
this.setState({'content': newContent, 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
}
handleSelectChange(id, e, index, val) {
this.setState({'content': this.state.content.set(id, val), 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
handleWorkflowChange(e, index, val) {
this.setState({'content': this.state.content.set('workflow_id', val), 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
handlePrintWorkflowChange(e, index, val) {
let expt = this.state.content.get('export');
if (!expt) {
expt = Immutable.fromJS({});
}
expt = expt.set('workflow_id', val);
this.setState({'content': this.state.content.set('export', expt), 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
handlePrintWorkflowSectionChange(e, index, val) {
let expt = this.state.content.get('export');
if (!expt) {
expt = Immutable.fromJS({});
}
expt = expt.set('workflow_section_id', val);
this.setState({'content': this.state.content.set('export', expt), 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
handlePrintIssueChange(e, index, val) {
let expt = this.state.content.get('export');
if (!expt) {
expt = Immutable.fromJS({});
}
expt = expt.set('issue_id', val);
this.setState({'content': this.state.content.set('export', expt), 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
onSelectUser(user) {
let expt = this.state.content.get('export');
if (!expt) {
expt = Immutable.fromJS({});
}
expt = expt.set('assignee_id', user.get('id')).set('assignee', user);
this.setState({'content': this.state.content.set('export', expt), 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
onRemoveUser(user) {
let expt = this.state.content.get('export');
if (!expt) {
expt = Immutable.fromJS({});
}
expt = expt.set('assignee_id', null).set('assignee', null);
this.setState({'content': this.state.content.set('export', expt), 'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
handleMetaFormChange(fieldData) {
if (fieldData) {
let newContent = this.state.content;
let meta = this.state.content.get('meta');
if (!meta) {
meta = Immutable.fromJS({});
}
meta = meta.set(fieldData.field, Immutable.fromJS(fieldData));
this.setState({'content': newContent.set('meta', meta), 'dirty': true});
}
}
handleEditorChange(raw, state, name) {
if (!this.state.dirty) {
this.setState({'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
this.state.contentLimbo = this.toHtml(state.getCurrentContent()).replace(/(<([^>]+)>)/ig, "");
this.contentData.raw = raw;
this.contentData.html = this.toHtml(state.getCurrentContent());
if (this.keyTimer !== null) {
clearTimeout(this.keyTimer);
}
this.keyTimer = setTimeout(this.handleDraftTimer.bind(this), 1000);
}
handleEditorChange2(e, raw, serializer) {
if (!this.state.dirty) {
this.setState({'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
this.state.contentLimbo = serializer(raw).replace(/(<([^>]+)>)/ig, "");
this.contentData.raw = raw.toJSON();
this.contentData.html = serializer(raw);
if (this.keyTimer !== null) {
clearTimeout(this.keyTimer);
}
this.keyTimer = setTimeout(this.handleDraftTimer.bind(this), 1000);
}
handleConvertContent(e) {
if (!confirm('Are you sure you want to convert this? Converting content to Editor2 will remove existing editorial notes and may require reformatting.')) {
return;
}
const raw = DraftSerializer.deserialize(JSON.parse(this.state.content.get('content_raw')));
const serializer = (data) => {
return '';
};
this.handleEditorChange2.call(this, e, raw, serializer);
this.setState({
content: this.state.content.set('content_raw', JSON.stringify(raw))
});
}
handleSimpleEditorChange(raw, state, name) {
if (!this.state.dirty) {
this.setState({'dirty': true});
this.props.dispatch(applicationIsDirty(true));
}
this.setState({
abstractLimbo: this.toHtml(state.getCurrentContent()).replace(/(<([^>]+)>)/ig, "")
});
this.abstractData.raw = raw;
this.abstractData.html = this.toHtml(state.getCurrentContent());
}
handleDraftTimer() {
if (!this.isNew() && this.isUserEditable()) {
this.props.draftable.shouldCreateDraft({
srn: this.state.content.get('srn'),
raw: this.contentData.raw,
html: this.contentData.html
});
}
}
handleEditorStateChange(refCount) {
if (refCount >= 1 && this.props.draftable) {
this.props.draftable.haltDraft(true);
} else {
this.props.draftable.haltDraft(false);
}
}
handleSaveCheckIn() {
this.handleSave(true);
}
handleSave(andCheckin = false) {
const {dispatch} = this.props;
var data = {
'slug': this.state.content.get('slug'),
'type': this.state.content.get('type'),
'title': this.state.content.get('title'),
'title_url': this.state.content.get('title_url'),
'layout_template': this.state.content.get('layout_template'),
'content_raw': this.contentData.raw ? this.contentData.raw : this.state.content.get('content_raw'),
'content': this.contentData.html ? this.contentData.html : this.state.content.get('content'),
'abstract_raw': this.abstractData.raw ? this.abstractData.raw : this.state.content.get('abstract_raw'),
'abstract': this.abstractData.html ? this.abstractData.html : this.state.content.get('abstract'),
'published_at': this.state.content.get('published_at'),
'workflow_id': this.state.content.get('workflow_id'),
'container_id': this.state.content.get('container_id'),
'weight': this.state.content.get('weight'),
'click_through': this.state.content.get('click_through')
};
if (!this.isUserEditable()) {
return;
}
dispatch(snackbarShowMessage('Saving...', false));
if (this.state.content.get('export')) {
data.export = {};
if (this.state.content.get('export').get('workflow_id')) {
data.export.workflow_id = this.state.content.get('export').get('workflow_id');
}
if (this.state.content.get('export').get('workflow_section_id')) {
data.export.workflow_section_id = this.state.content.get('export').get('workflow_section_id');
}
if (this.state.content.get('export').get('issue_id')) {
data.export.issue_id = this.state.content.get('export').get('issue_id');
}
if (this.state.content.get('export').get('assignee_id')) {
data.export.assignee_id = this.state.content.get('export').get('assignee_id');
}
}
// This is set by the callback in the UrlHandler component
if (this.state.customUrl) {
data.urls = [{url: this.state.customUrl, is_default: 1}];
}
if (this.state.content.get('meta') && this.state.content.get('meta').size) {
data.meta = this.state.content.get('meta').toJS();
}
if (this.state.content.get('authors') && this.state.content.get('authors').size) {
data['authors'] = this.state.content.get('authors').map((author, i) => {
return author.get('uuid');
});
} else {
data['authors'] = [];
}
if (this.state.content.get('tags') && this.state.content.get('tags').size) {
data['tags'] = this.state.content.get('tags').map((tag, i) => {
return tag.get('uuid');
});
} else {
data['tags'] = [];
}
if (this.state.content.get('ssts_id')) {
data['ssts_id'] = this.state.content.get('ssts_id');
} else {
data['ssts_id'] = null;
}
if (this.state.content.get('dominantAttachment')) {
data['dominantAttachment'] = this.state.content.get('dominantAttachment').get('uuid');
} else {
data['dominantAttachment'] = false;
}
// the assignment query param is set by the assignment-item's "Create content" button
if (this.props.location.query && this.props.location.query.assignment) {
const assignment = this.props.assignments.items.find((item) => item.get('uuid') == this.props.location.query.assignment);
data['assignment_id'] = assignment.size ? assignment.get('id') : null;
}
if (!this.isNew()) {
// Update existing is pretty straightforward
// update via the uuid
dispatch(contentUpdate(this.getUuid(), data))
.then(() => {
// reset the object to the new item
const content = this.props.content.items.find((item) => item.get('uuid') == this.state.content.get('uuid'));
this.setState({'content': content});
})
.then(() => {
// flash the message
dispatch(snackbarShowMessage('Content updated'));
this.setState({'dirty': false});
dispatch(applicationIsDirty(false));
})
.then(() => {
// if we're checking in too, do that
if (andCheckin !== true) {
return;
}
this.handleCheckin();
})
.catch(() => {
dispatch(snackbarClose());
dispatch(alertShowMessage({
'title': 'Oops',
'message': (
<p>There was a problem saving your content. Usually this means you don't have permission to perform an action, like publishing or deleting.</p>
)
}));
this.setState({'dirty': true});
dispatch(applicationIsDirty(true));
});
} else {
// Create is slightly different
// first, create it
dispatch(contentCreate(data))
.then(() => {
// then grab the newly created item and pass it on
const content = this.props.content.items.find((item) => item.get('uuid') == this.props.content.selected);
dispatch(snackbarShowMessage('Content created'));
return content;
})
.then((content) => {
// update the state with the newly created object
this.setState({
'dirty': false,
'content': content
}, () => {
// after that's done check it out
dispatch(applicationIsDirty(false));
this.handleCheckout({})
.then(() => {
// when the checkout is complete, update the URL
// if you don't wait, you can get a race condition that can
// result in a no-op warning
browserHistory.push('/ceo/content/' + this.props.content.selected);
});
});
})
.catch(() => {
dispatch(snackbarClose());
dispatch(alertShowMessage({
'title': 'Oops',
'message': (
<p>There was a problem saving your content. Usually this means you don't have permission to perform an action, like publishing or deleting.</p>
)
}));
this.setState({'dirty': true});
dispatch(applicationIsDirty(true));
});
}
}
handlePreview() {
const url = '/content/preview/' + this.state.content.get('uuid');
window.open(url, 'preview');
}
handleOpenPublish() {
if (this.props.application.settings.get('publicationIsLocked')) {
this.props.dispatch(alertShowMessage({
'title': 'Publication Locked',
'message': (
<p>Publication is currently locked. Please have an administrator <a href="https://snworks.zendesk.com/hc/en-us/requests/new?__cifsub=CEO+Billing+Issue" target="_blank">contact support</a> for more information.</p>
)
}));
return;
}
this.setState({'publishIsOpen': true});
}
handleClosePublish() {
this.setState({'publishIsOpen': false});
}
handleRestore(content) {
this.setState({'content': content});
}
handleOpenDiff() {
this.setState({'diffIsOpen': true});
}
handleCloseDiff() {
this.setState({'diffIsOpen': false});
}
handleOpenExport() {
this.setState({'exportIsOpen': true});
}
handleCloseExport() {
this.setState({'exportIsOpen': false});
}
handleOpenDomMedia() {
this.setState({'domMediaIsOpen': true});
}
handleCloseDomMedia() {
this.setState({'domMediaIsOpen': false});
}
onPublish(pubData) {
if (pubData.abstract !== null) {
this.abstractData.html = pubData.abstract;
this.abstractData.raw = pubData.abstract_raw;
}
const newContent = this.state.content.set('published_at', pubData.published_at.toString())
this.setState({'content': newContent}, () => {
this.handleSave();
});
}
handleCancelPublish() {
const newContent = this.state.content.set('published_at', null)
this.setState({'content': newContent}, () => {
this.handleSave();
});
}
// callbacks to handle the author selection
onSelectAuthor(author) {
let authors = this.state.content.get('authors');
if (!authors) {
authors = Immutable.fromJS({});
}
const existing = authors.find((item) => item.get('uuid') == author.get('uuid'));
if (existing) {
return;
}
authors = authors.push(author);
this.setState({
'content': this.state.content.set('authors', authors),
'dirty': true
});
this.props.dispatch(applicationIsDirty(true));
}
onRemoveAuthor(author) {
let authors = this.state.content.get('authors');
let newAuthors = authors.filter((item, i) => {
return author.get('uuid') != item.get('uuid');
});
this.setState({
'content': this.state.content.set('authors', newAuthors),
'dirty': true
});
this.props.dispatch(applicationIsDirty(true));
}
// callbacks to handle the tag selection
onSelectTag(tag) {
let tags = this.state.content.get('tags');
if (!tags) {
tags = Immutable.fromJS({});
}
const existing = tags.find((item) => item.get('uuid') == tag.get('uuid'));
if (existing) {
return;
}
tags = tags.push(tag);
this.setState({
'content': this.state.content.set('tags', tags),
'dirty': true
});
this.props.dispatch(applicationIsDirty(true));
}
onRemoveTag(tag) {
let tags = this.state.content.get('tags');
let newTags = tags.filter((item, i) => {
return tag.get('uuid') != item.get('uuid');
});
this.setState({
'content': this.state.content.set('tags', newTags),
'dirty': true
});
this.props.dispatch(applicationIsDirty(true));
}
onUpdateSsts(ssts) {
this.setState({
'content': this.state.content.set('ssts_id', ssts.id),
'dirty': true
});
this.props.dispatch(applicationIsDirty(true));
}
handleSelectDomMedia(media) {
const newContent = this.state.content.set('dominantAttachment', media);
this.setState({
'content': newContent,
'dirty': true
});
this.handleCloseDomMedia.call(this);
this.props.dispatch(applicationIsDirty(true));
}
handleRemoveDomMedia() {
if (!this.isUserEditable()) {
return;
}
const newContent = this.state.content.set('dominantAttachment', null);
this.setState({
'content': newContent,
'dirty': true
});
this.props.dispatch(applicationIsDirty(true));
}
handleSyncFromPrint(e) {
const {dispatch} = this.props;
if (confirm('Are you sure you want to sync this story from print? It will overwrite any changes in the web version.')) {
return dispatch(contentSyncPrint(this.getUuid()))
.then((data) => {
// const content = this.props.content.items.find((item) => item.get('uuid') == this.state.edited.uuid);
let content = data.first()
if (!data.first().get('content_raw')) {
// no content raw, reload the page
// browserHistory.push("/ceo/redirect?next=content/" + this.state.content.get('uuid'));
content = content.set('content_raw', content.get('content').replace(/\n/g, ""));
}
this.setState({'content': content});
dispatch(snackbarShowMessage('Content synced'));
});
}
}
handleClearLock(e) {
if (!CurrentUser.hasRole('Administrator')) {
return;
}
if (!confirm('Are you sure you want to clear this lock? You may overwrite pending changes.')) {
return;
}
const {dispatch} = this.props;
return dispatch(contentClearLock(CurrentUser.getUuid(), this.state.content.get('lock').get('uuid')))
.then(() => dispatch(contentFetchOne(this.getUuid())))
.then(() => {
const content = this.props.content.items.find((item) => item.get('uuid') == this.state.content.get('uuid'));
this.setState({'content': content});
});
}
render() {
if (!this.isNew() && !this.state.content.get('uuid')) {
return (
<div className="content-item-root">
<Row>
<Col xs={12}>
<Paper className='padded clear-top clear-bottom'>
<LoadingIndicator />
</Paper>
</Col>
</Row>
</div>
);
}
let attachment = false;
if (this.state.content.get('attachment') && this.state.content.get('attachment').get('id')) {
attachment = true;
}
return (
<div className="content-item-root">
<Row>
<Col xs={12}>
<Paper className='toolbar' style={{ paddingTop:'10px', paddingBottom:'10px' }}>
<Row middle='xs'>
<Col xs={8}>
<ContentLock
onSave={this.handleSave}
onSaveCheckIn={this.handleSaveCheckIn}
onCheckIn={this.handleCheckin}
onCheckOut={this.handleCheckout}
userEditable={this.isUserEditable()}
content={this.state.content}
/>
</Col>
<Col xs={4} end='xs'>
<FlatButton
className='action-preview'
onClick={this.handlePreview}
disabled={this.isNew() ? true : false}
label={"View " + (this.state.content.get('type') ? this.state.content.get('type') : 'Article')}
icon={<FontIcon className='mui-icons'>desktop_mac</FontIcon>}
/>
</Col>
</Row>
</Paper>
<Row top='xs'>
<Col xs={9}>
<Paper className='padded'>
<Row>
<Col xs={attachment ? 6 : 12}>
{
// because of the way the sluggify works, the `|| ''` bit in the value property
// avoids an invariant warning bug in the test suite
}
<ContentTitleUrlBox
disabled={this.isUserEditable() ? false : true}
content={this.state.content}
handleFormChange={this.handleFormChange}
onChange={this.onChange}
isNew={this.isNew() ? true : false}
/>
</Col>
{
attachment
? <ContentAttachmentView isUserEditable={this.isUserEditable()} content={this.state.content} dispatch={this.props.dispatch}/>
: ''
}
</Row>
<Row>
<Col xs={12}>
<ContentMetaProperties
group='content'
disabled={this.isUserEditable()}
onChange={this.handleMetaFormChange.bind(this)}
content={this.state.content}
/>
{
this.state.content.get('type') != 'page'
? (
<ContentAuthors
authors={this.state.content.get('authors')}
disabled={this.isUserEditable() ? false : true}
onRemoveAuthor={this.onRemoveAuthor.bind(this)}
onSelectAuthor={this.onSelectAuthor.bind(this)}
/>
)
: ''
}
<ContentTypeBox
content={this.state.content}
containers={this.props.containers}
disabled={this.isUserEditable() ? false : true}
handleSelectChange={this.handleSelectChange.bind(this)}
/>
<ContentTags
tags={this.state.content.get('tags')}
disabled={this.isUserEditable() ? false : true}
onRemoveTag={this.onRemoveTag.bind(this)}
onSelectTag={this.onSelectTag.bind(this)}
/>
{
this.props.application.settings.get('ssts')
? (
<SstsSearchBox
value={this.state.content.get('ssts_path')}
onChange={this.onUpdateSsts.bind(this)}
/>
)
: ''
}
</Col>
</Row>
<ContentRichEditorContainer
lastSaved={this.state.lastSaved}
pendingDraft={this.state.pendingDraft}
handleRestoreDraft={this.handleRestoreDraft.bind(this)}
disabled={this.isUserEditable() ? false : true}
handleEditorChange={this.handleEditorChange}
handleEditorChange2={this.handleEditorChange2}
convertContent={this.handleConvertContent.bind(this)}
handleStateChange={this.handleEditorStateChange.bind(this)}
content={this.state.content}
contentLimbo={this.state.contentLimbo}
editor2={this.props.application.settings.get('labsEditor2') ? true : false}
useSmartQuotes={this.props.application.settings.get('educateQuotes') ? true : false}
useSuggestedLinks={this.props.application.settings.get('relatedContent') ? true : false}
/>
<ContentAbstractEditorContainer
disabled={this.isUserEditable() ? false : true}
content={this.state.content}
abstractLimbo={this.state.abstractLimbo}
handleEditorChange={this.handleSimpleEditorChange.bind(this)}
/>
</Paper>
<Paper className='clear-top padded'>
<Subheader>Meta Properties</Subheader>
<ContentMetaProperties
group='content-lower'
disabled={this.isUserEditable()}
onChange={this.handleMetaFormChange.bind(this)}
content={this.state.content}
/>
</Paper>
</Col>
<Col xs={3}>
{
attachment
? <ContentAttachmentPreview content={this.state.content} dispatch={this.props.dispatch}/>
: ''
}
<ContentStatusBox
content={this.state.content}
handleOpenDiff={this.handleOpenDiff.bind(this)}
handleWorkflowChange={this.handleWorkflowChange.bind(this)}
workflows={this.props.workflows}
disabled={this.isUserEditable() ? false : true}
/>
{
Config.get('cclicense') && (!this.state.content.get('type') || this.state.content.get('type') == 'article')
? (
<ContentPrintStatusBox
content={this.state.content}
handlePrintWorkflowChange={this.handlePrintWorkflowChange.bind(this)}
handlePrintWorkflowSectionChange={this.handlePrintWorkflowSectionChange.bind(this)}
handlePrintIssueChange={this.handlePrintIssueChange.bind(this)}
onSelectUser={this.onSelectUser.bind(this)}
onRemoveUser={this.onRemoveUser.bind(this)}
workflows={this.props.workflows}
workflowSections={this.props.workflowSections}
assignments={this.props.assignments}
issues={this.props.issues}
handleSyncFromPrint={this.handleSyncFromPrint.bind(this)}
disabled={this.isUserEditable() ? false : true}
/>
)
: ''
}
<ContentPublishBox
content={this.state.content}
handleFormChange={this.handleFormChange.bind(this)}
disabled={this.isUserEditable() ? false : true}
handleCancelPublish={this.handleCancelPublish.bind(this)}
handleOpenPublish={this.handleOpenPublish}
/>
{
this.props.application.settings.get('relatedContent')
? (
<ContentRelatedBox
content={this.state.content}
/>
)
: ''
}
<DominantAttachmentBox
content={this.state.content}
handleRemoveDomMedia={this.handleRemoveDomMedia.bind(this)}
handleOpenDomMedia={this.handleOpenDomMedia.bind(this)}
/>
<ContentAssignment content={this.state.content} />
<div className='clear-bottom'>
<RaisedButton
label='Export'
disabled={this.state.content.get('uuid') ? false : true}
style={{'width':'100%'}}
onClick={this.handleOpenExport.bind(this)}
icon={<FontIcon className='mui-icons'>import_export</FontIcon>}
/>
</div>
{
this.isLocked()
? (
<div className='clear-bottom'>
<RaisedButton
label='Clear Lock'
style={{width:'100%'}}
icon={<FontIcon className='mui-icons'>clear</FontIcon>}
onClick={this.handleClearLock.bind(this)}
disabled={CurrentUser.hasRole('Administrator') && !this.isNew() ? false : true}
/>
</div>
)
: ''
}
<div className='clear-bottom'>
<DeleteButton
className='full'
fullWidth={true}
icon={true}
disabled={this.isNew() || !this.isUserEditable() ? true : false} onDelete={this.handleDelete}
/>
</div>
<NoteViewer editor="content" version={this.state.content.get('version')} contentUuid={this.state.content ? this.state.content.get('uuid') : false} />
</Col>
</Row>
{
this.state.publishIsOpen && this.state.content.get('type') != 'media'
? (
<PublishModal
content={this.state.content}
isOpen={this.state.publishIsOpen}
onPublish={this.onPublish.bind(this)}
onClose={this.handleClosePublish.bind(this)}
abstract={(this.abstractData.html && this.abstractData.html.length) ? this.abstractData : false}
/>
)
: ''
}
{
this.state.publishIsOpen && this.state.content.get('type') == 'media'
? (
<SimplePublishModal
content={this.state.content}
isOpen={this.state.publishIsOpen}
onPublish={this.onPublish.bind(this)}
onClose={this.handleClosePublish.bind(this)}
abstract={(this.abstractData.html && this.abstractData.html.length) ? this.abstractData : false}
/>
)
: ''
}
{
this.state.diffIsOpen
? (
<DiffModal
content={this.state.content}
isOpen={this.state.diffIsOpen}
disabled={this.isUserEditable() ? false : true}
onRestore={this.handleRestore.bind(this)}
onRequestClose={this.handleCloseDiff.bind(this)}
logs={this.state.content.get('audit')}
/>
)
: ''
}
{
this.state.domMediaIsOpen
? (
<DominantMediaModal
content={this.state.content}
isOpen={this.state.domMediaIsOpen}
onRequestClose={this.handleCloseDomMedia.bind(this)}
onSelectMedia={this.handleSelectDomMedia.bind(this)}
/>
)
: ''
}
{
this.state.content.get('uuid')
? (
<ExportModal
content={this.state.content}
isOpen={this.state.exportIsOpen}
onRequestClose={this.handleCloseExport.bind(this)}
/>
)
: ''
}
</Col>
</Row>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
workflows: state.workflows,
content: state.content,
assignments: state.assignments,
containers: state.containers,
workflowSections: state.workflowSections,
issues: state.issues,
globalHistory: state.globalHistory,
application: state.application
};
}
class ContentTitleUrlBox extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (!Immutable.is(this.props.content.get('urls'), nextProps.content.get('urls'))) {
return true;
}
if (this.props.content.get('title') != nextProps.content.get('title')) {
return true;
}
if (this.props.content.get('slug') != nextProps.content.get('slug')) {
return true;
}
if (this.props.content.get('title_url') != nextProps.content.get('title_url')) {
return true;
}
if (this.props.content.get('click_through') != nextProps.content.get('click_through')) {
return true;
}
return false;
}
componentDidMount() {
if (this.refs.slugTextField && !this.refs.slugTextField.input.value) {
setTimeout(() => this.refs.slugTextField.focus(), 0);
}
}
render() {
return (
<div>
<TextField
disabled={this.props.disabled}
onChange={this.props.handleFormChange}
value={this.props.content.get('slug') || ''}
htmlFor='slug'
floatingLabelText="Slug"
hintText='Enter a Slug for the content'
fullWidth={true}
ref='slugTextField'
/>
<TextField
disabled={this.props.disabled}
onChange={this.props.handleFormChange}
value={this.props.content.get('title_url') || ''}
htmlFor='title_url'
floatingLabelText="URL"
hintText='Enter a URL for the content'
fullWidth={true}
/>
<TextField
disabled={this.props.disabled}
onChange={this.props.handleFormChange}
value={this.props.content.get('title')}
htmlFor='title'
floatingLabelText="Title"
hintText='Enter a descriptive Title for the content'
fullWidth={true}
/>
<small>
<CharacterCounter count={this.props.content.get('title')} displayEmpty={true} />
</small>
{
!this.props.isNew
? (
<div>
<UrlHandler disabled={this.props.disabled} onChange={this.props.onChange} content={this.props.content} />
</div>
)
: ''
}
{
this.props.content.get('type') == 'media'
? (
<TextField
disabled={this.props.disabled}
onChange={this.props.handleFormChange}
value={this.props.content.get('click_through')}
htmlFor='click_through'
floatingLabelText="Click Through URL"
hintText='Custom click through URL for media file'
fullWidth={true}
/>
)
: ''
}
</div>
);
}
}
class ContentTimeList extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.modified_at != nextProps.modified_at
|| this.props.created_at != nextProps.created_at) {
return true;
}
return false
}
render() {
return (
<List>
<ListItem
disabled={true}
primaryText={timeToFormat(this.props.modified_at, 'LT l')}
secondaryText='last modified time'
/>
<ListItem
disabled={true}
primaryText={timeToFormat(this.props.created_at, 'LT l')}
secondaryText='created time'
/>
</List>
)
}
}
class ContentAuthors extends React.Component {
onRemoveAuthor(author) {
this.props.onRemoveAuthor(author);
}
onSelectAuthor(author) {
this.props.onSelectAuthor(author);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (!Immutable.is(nextProps.authors, this.props.authors)) {
return true;
}
return false;
}
render() {
return (
<Row middle='xs' flexy={true}>
<Col flex={0} style={{'paddingRight': this.props.authors.size ? '10px' : 0}}>
{this.props.authors.map((author, i) => {
return (
<div
style={{'marginTop': '25px'}}
className='pill'
key={i}
>
{author.get('name')}
<FontIcon
className='mui-icons'
onClick={this.onRemoveAuthor.bind(this, author)}
>close</FontIcon>
</div>
);
})}
</Col>
<Col flex={5}>
<AuthorSearchBox
onSelectAuthor={this.onSelectAuthor.bind(this)}
disabled={this.props.disabled}
/>
</Col>
</Row>
);
}
}
class ContentTags extends React.Component {
onRemoveTag(tag) {
this.props.onRemoveTag(tag);
}
onSelectTag(tag) {
this.props.onSelectTag(tag);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (!Immutable.is(nextProps.tags, this.props.tags)) {
return true;
}
return false;
}
render() {
return (
<Row middle='xs' flexy={true}>
<Col flex={0} style={{'paddingRight': this.props.tags.size ? '10px' : 0}}>
{this.props.tags.map((tag, i) => {
return (
<div
style={{'marginTop': '25px'}}
className='pill'
key={i}
>
{tag.get('name')}
<FontIcon
className='mui-icons'
onClick={this.onRemoveTag.bind(this, tag)}
>close</FontIcon>
</div>
);
})}
</Col>
<Col flex={5}>
<TagSearchBox
onSelectTag={this.onSelectTag.bind(this)}
disabled={this.props.disabled}
/>
</Col>
</Row>
);
}
}
class ContentTypeBox extends React.Component {
handleSelectChange(id, e, index, val) {
if (val == 'media') {
return;
}
this.props.handleSelectChange(id, e, index, val);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (this.props.content.get('type') != nextProps.content.get('type')) {
return true;
}
if (this.props.content.get('layout_template') != nextProps.content.get('layout_template')) {
return true;
}
if (this.props.content.get('container_id') != nextProps.content.get('container_id')) {
return true;
}
if (this.props.containers != nextProps.containers) {
return true;
}
return false;
}
render() {
let disabled = this.props.disabled;
const content = this.props.content;
let content_types = [];
if (Config && Config.get('content_types')) {
content_types = Config.get('content_types');
}
if (this.props.content.get('uuid') && this.props.content.get('type') == 'media') {
disabled = true;
}
let layout_templates = [];
if (Config && Config.get('layout_templates')) {
const all_layout_templates = Config.get('layout_templates');
layout_templates = all_layout_templates[content.get('type')] ? all_layout_templates[content.get('type')] : [] ;
}
return (
<Row middle='xs'>
<Col xs={6}>
<SelectField
fullWidth={true}
floatingLabelText="Content Type"
value={content.get('type')}
onChange={this.handleSelectChange.bind(this, 'type')}
disabled={disabled}
>
{content_types.map((type, i) => {
return <MenuItem key={i} value={type} primaryText={type} />;
})}
</SelectField>
</Col>
<Col xs={6}>
<SelectField
fullWidth={true}
floatingLabelText="Layout Template"
value={content.get('layout_template')}
onChange={this.handleSelectChange.bind(this, 'layout_template')}
disabled={this.props.disabled}
>
<MenuItem value={false} primaryText='None' />
{layout_templates.map((template, i) => {
return <MenuItem key={i} value={template} primaryText={template} />;
})}
</SelectField>
</Col>
{
content.get('type') == 'post'
? (
<Col xs={12}>
<SelectField
fullWidth={true}
floatingLabelText="Blog"
value={content.get('container_id')}
onChange={this.handleSelectChange.bind(this, 'container_id')}
disabled={disabled}
>
{this.props.containers.items.map((container, i) => {
return <MenuItem key={i} value={container.get('id')} primaryText={container.get('title')} />;
})}
</SelectField>
</Col>
)
: ''
}
</Row>
);
}
}
class ContentStatusBox extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.content.get('modified_at') != nextProps.content.get('modified_at')
|| this.props.content.get('created_at') != nextProps.content.get('created_at')) {
return true;
}
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (this.props.content.get('version') != nextProps.content.get('version')) {
return true;
}
if (!Immutable.is(this.props.content.get('versions'), nextProps.content.get('versions'))) {
return true;
}
if (this.props.content.get('workflow_id') != nextProps.content.get('workflow_id')) {
return true;
}
if (!Immutable.is(this.props.content.get('export'), nextProps.content.get('export'))) {
return true;
}
if (!Immutable.is(this.props.workflows.items, nextProps.workflows.items)) {
return true;
}
return false;
}
render() {
return (
<Paper className='clear-bottom'>
<ContentTimeList
modified_at={this.props.content.get('modified_at')}
created_at={this.props.content.get('created_at')}
/>
<List>
<Subheader>Web</Subheader>
<ContentVersionListItem
handleOpenDiff={this.props.handleOpenDiff}
content={this.props.content}
disabled={this.props.disabled}
/>
<ContentWebWorkflowListItem
handleWorkflowChange={this.props.handleWorkflowChange}
content={this.props.content}
workflows={this.props.workflows}
disabled={this.props.disabled}
/>
</List>
</Paper>
);
}
}
class ContentPrintStatusBox extends React.Component {
onSelectUser(user) {
this.props.onSelectUser(user);
}
onRemoveUser(user) {
this.props.onRemoveUser(user);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.content.get('modified_at') != nextProps.content.get('modified_at')
|| this.props.content.get('created_at') != nextProps.content.get('created_at')) {
return true;
}
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (this.props.content.get('version') != nextProps.content.get('version')) {
return true;
}
if (!Immutable.is(this.props.content.get('versions'), nextProps.content.get('versions'))) {
return true;
}
if (this.props.content.get('workflow_id') != nextProps.content.get('workflow_id')) {
return true;
}
if (!Immutable.is(this.props.content.get('export'), nextProps.content.get('export'))) {
return true;
}
if (!Immutable.is(this.props.workflows.items, nextProps.workflows.items)) {
return true;
}
return false;
}
render() {
return (
<Paper className='clear-bottom'>
<List>
<Subheader>Print</Subheader>
<ContentPrintVersionListItem
content={this.props.content}
/>
<ContentPrintWorkflowListItem
handlePrintWorkflowChange={this.props.handlePrintWorkflowChange}
content={this.props.content}
workflows={this.props.workflows}
disabled={this.props.disabled}
/>
<ContentPrintWorkflowSectionListItem
handlePrintWorkflowSectionChange={this.props.handlePrintWorkflowSectionChange}
content={this.props.content}
workflows={this.props.workflow}
workflowSections={this.props.workflowSections}
disabled={this.props.disabled}
/>
<ContentPrintIssueListItem
handlePrintIssueChange={this.props.handlePrintIssueChange}
content={this.props.content}
issues={this.props.issues}
disabled={this.props.disabled}
/>
{!this.props.content.get('uuid') || !this.props.content.get('export') || !parseInt(this.props.content.get('export').get('assignee_id')) || !this.props.content.get('export').get('assignee')
? (
<div>
<Subheader>Assignee</Subheader>
<ListItem disabled={true} style={{marginTop:'-16px', paddingTop:'0px'}}>
<UserSearchBox
onSelectUser={this.onSelectUser.bind(this)}
disabled={this.props.disabled}
/>
</ListItem>
</div>
)
: (
<ListItem
disabled={true}
primaryText={this.props.content.get('export').get('assignee').get('name')}
rightIcon={
<FontIcon
className='mui-icons '
style={{'cursor': 'pointer'}}
onClick={this.onRemoveUser.bind(this, this.props.content.get('export').get('assignee'))}
>close</FontIcon>
}
>
</ListItem>
)
}
<div className='center-align clear-top'>
<RaisedButton
style={{width:'90%'}}
label='update from print'
labelStyle={{fontSize:'13px'}}
className='secondary-button'
onClick={this.props.handleSyncFromPrint}
disabled={this.props.disabled}
icon={<FontIcon className='mui-icons'>sync</FontIcon>}
/>
</div>
</List>
</Paper>
)
}
}
class ContentVersionListItem extends React.Component {
handleOpenDiff(e) {
this.props.handleOpenDiff(e);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.content.get('version') != nextProps.content.get('version')) {
return true;
}
if (!Immutable.is(this.props.content.get('versions'), nextProps.content.get('versions'))) {
return true;
}
if (!Immutable.is(this.props.content.get('user'), nextProps.content.get('user'))) {
return true;
}
return false;
}
render() {
let name = '';
if(parseInt(this.props.content.get('user_id')) && this.props.content.get('user')) {
name = ' ' + this.props.content.get('user').get('name');
}
return (
<ListItem
style={{color:ceoTheme.palette.primary3Color}}
onClick={this.handleOpenDiff.bind(this)}
disabled={parseInt(this.props.content.get('version')) && parseInt(this.props.content.get('version')) <= 1 ? true : false}
>
{
this.props.content.get('version')
? 'v'+this.props.content.get('version')
: 'New'
}
{name}
</ListItem>
)
}
}
class ContentWebWorkflowListItem extends React.Component {
handleWorkflowChange(e, index, val) {
this.props.handleWorkflowChange(e, index, val);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (!Immutable.is(this.props.workflows.items, nextProps.workflows.items)) {
return true;
}
if (this.props.content.get('workflow_id') != nextProps.content.get('workflow_id')) {
return true;
}
return false;
}
render() {
return (
<ListItem disabled={true} style={{marginTop:'-16px', paddingTop:'0px'}}>
<SelectField
fullWidth={true}
autoWidth={true}
value={parseInt(this.props.content.get('workflow_id')) ? this.props.content.get('workflow_id') : null}
onChange={this.handleWorkflowChange.bind(this)}
floatingLabelText='Workflow Status'
disabled={this.props.disabled}
>
{this.props.workflows.filters.contentOnly.map((id, i) => {
const wf = this.props.workflows.items.find((workflow) => {
return workflow.get('uuid') == id;
})
return <MenuItem key={i} value={wf.get('id')} primaryText={wf.get('name')} />
})}
</SelectField>
</ListItem>
);
}
}
class ContentPrintVersionListItem extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (!Immutable.is(this.props.content.get('export'), nextProps.content.get('export'))) {
return true;
}
return false;
}
render() {
return (
<ListItem>
{
this.props.content.get('export')
? 'v' + this.props.content.get('export').get('version')
: 'No print version'
}
{
this.props.content.get('export') && parseInt(this.props.content.get('export').get('user_id')) && this.props.content.get('export').get('user')
? ' ' + this.props.content.get('export').get('user').get('name')
: ''
}
</ListItem>
)
}
}
class ContentPrintWorkflowListItem extends React.Component {
handlePrintWorkflowChange(e, index, val) {
this.props.handlePrintWorkflowChange(e, index, val);
}
shouldComponentUpdate(nextProps, nextState) {
if (!Immutable.is(this.props.workflows.items, nextProps.workflows.items)) {
return true;
}
if (this.props.content.get('export') != nextProps.content.get('export')) {
return true;
}
return false;
}
render() {
return (
<ListItem disabled={true} style={{marginTop:'-16px', paddingTop:'0px'}}>
<SelectField
fullWidth={true}
autoWidth={true}
value={this.props.content.get('export') && parseInt(this.props.content.get('export').get('workflow_id')) ? this.props.content.get('export').get('workflow_id') : null}
onChange={this.handlePrintWorkflowChange.bind(this)}
floatingLabelText='Workflow Status'
disabled={this.props.disabled}
>
{this.props.workflows.filters.printOnly.map((id, i) => {
const wf = this.props.workflows.items.find((workflow) => {
return workflow.get('uuid') == id;
})
return <MenuItem key={i} value={wf.get('id')} primaryText={wf.get('name')} />
})}
</SelectField>
</ListItem>
);
}
}
class ContentPrintWorkflowSectionListItem extends React.Component {
handlePrintWorkflowSectionChange(e, index, val) {
this.props.handlePrintWorkflowSectionChange(e, index, val);
}
shouldComponentUpdate(nextProps, nextState) {
if (!Immutable.is(this.props.workflowSections.items, nextProps.workflowSections.items)) {
return true;
}
if (this.props.content.get('export') != nextProps.content.get('export')) {
return true;
}
return false;
}
render() {
return (
<ListItem disabled={true} style={{marginTop:'-16px', paddingTop:'0px'}}>
<SelectField
fullWidth={true}
autoWidth={true}
value={this.props.content.get('export') && parseInt(this.props.content.get('export').get('workflow_section_id')) ? this.props.content.get('export').get('workflow_section_id') : null}
onChange={this.handlePrintWorkflowSectionChange.bind(this)}
floatingLabelText='Workflow Section'
disabled={this.props.disabled}
>
{this.props.workflowSections.items.map((workflowSection, i) => {
return <MenuItem key={i} value={workflowSection.get('id')} primaryText={workflowSection.get('name')} />
})}
</SelectField>
</ListItem>
);
}
}
class ContentPrintIssueListItem extends React.Component {
handlePrintIssueChange(e, index, val) {
this.props.handlePrintIssueChange(e, index, val);
}
shouldComponentUpdate(nextProps, nextState) {
if (!Immutable.is(this.props.issues.items, nextProps.issues.items)) {
return true;
}
if (this.props.content.get('export') != nextProps.content.get('export')) {
return true;
}
return false;
}
render() {
// Allows existing items attached to closed issues
// to still show the issue in the select drop down
let useIssue = null;
let didUseIssue = false;
if (this.props.content.get('export')
&& this.props.content.get('export').get('issue')
&& !parseInt(this.props.content.get('export').get('issue').get('status')) ) {
let tempIssue = this.props.content.get('export').get('issue');
let useLabel = [];
if (tempIssue.get('published_at')) {
useLabel.push(timeToFormat(tempIssue.get('published_at'), 'M/DD/YYYY'));
}
if (tempIssue.get('label')) {
useLabel.push(tempIssue.get('label'));
}
useIssue = tempIssue.set('decorated_label', useLabel.join(' - '));
}
return (
<ListItem disabled={true} style={{marginTop:'-16px', paddingTop:'0px'}}>
<SelectField
fullWidth={true}
autoWidth={true}
value={this.props.content.get('export') && parseInt(this.props.content.get('export').get('issue_id')) ? this.props.content.get('export').get('issue_id') : null}
onChange={this.handlePrintIssueChange.bind(this)}
floatingLabelText='Issue'
disabled={this.props.disabled}
>
{this.props.issues.open.map((uuid, i) => {
const issue = this.props.issues.items.find((item) => item.get('uuid') == uuid);
if (useIssue && issue.get('uuid') == useIssue.get('uuid')) {
didUseIssue = true;
}
let label = [];
if (issue.get('published_at')) {
label.push(timeToFormat(issue.get('published_at'), 'M/DD/YYYY'));
}
if (issue.get('label')) {
label.push(issue.get('label'));
}
return <MenuItem key={i} value={issue.get('id')} primaryText={label.join(' - ')} />
})}
{
useIssue && !didUseIssue
? <MenuItem value={useIssue.get('id')} primaryText={useIssue.get('decorated_label')} />
: ''
}
</SelectField>
</ListItem>
);
}
}
class ContentPublishBox extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (this.props.content.get('published_at') != nextProps.content.get('published_at')) {
return true;
}
if (this.props.content.get('weight') != nextProps.content.get('weight')) {
return true;
}
return false;
}
handleFormChange(e) {
this.props.handleFormChange(e);
}
handleCancelPublish() {
this.props.handleCancelPublish();
}
handleOpenPublish() {
this.props.handleOpenPublish();
}
render() {
return (
<Paper className='clear-bottom'>
<List>
<Subheader>Published</Subheader>
{
this.props.content.get('published_at')
? (
<ListItem
disabled={true}
primaryText={this.props.content.get('published_at') ? timeToFormat(this.props.content.get('published_at'), 'LT l') : 'Not published'}
leftIcon={<FontIcon className='mui-icons'>check</FontIcon>}
/>
)
: (
<ListItem
disabled={true}
primaryText='Not Published'
leftIcon={<FontIcon className='mui-icons'>error_outline</FontIcon>}
/>
)
}
<Subheader>Weight</Subheader>
<ListItem disabled={true} hoverColor='#FFFFFF' style={{marginTop:'-16px', paddingTop:'0px'}}>
<TextField
disabled={this.props.disabled}
onChange={this.handleFormChange.bind(this)}
value={this.props.content.get('weight')}
htmlFor='weight'
fullWidth={true}
type="number"
min={0}
max={5}
/>
</ListItem>
{
this.props.content.get('published_at')
? (
<div className='center-align'>
<RaisedButton
style={{width:'90%'}}
className='secondary-button'
onClick={this.handleCancelPublish.bind(this)}
disabled={this.props.disabled}
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.props.disabled}
label="Publish"
icon={<FontIcon className='mui-icons'>cloud_upload</FontIcon>}
/>
</div>
)
}
</List>
</Paper>
);
}
}
class DominantAttachmentBox extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (!Immutable.is(this.props.content.get('dominantAttachment'), nextProps.content.get('dominantAttachment'))) {
return true;
}
return false;
}
editAttachment(url, e) {
browserHistory.push(url);
window._ceoScrollTop();
}
render() {
let linkLocation = '';
if (this.props.content.get('dominantAttachment')) {
linkLocation = '/ceo/locate/' + this.props.content.get('dominantAttachment').get('uuid');
}
const avatars = {
gallery: 'collections',
image: 'photo',
video: 'videocam',
pdf: 'picture_as_pdf',
vimeo: 'ondemand_video',
youtube: 'ondemand_video',
audio: 'audiotrack'
}
let actionIcon = '';
if (this.props.content.get('dominantAttachment')) {
actionIcon = avatars[this.props.content.get('dominantAttachment').get('attachment').get('type')];
}
return (
<div className='clear-bottom'>
{
this.props.content.get('dominantAttachment')
? (
<Paper>
<Subheader>Dominant Media</Subheader>
<GridList style={{padding:'0px 16px 16px'}}>
<GridTile
cols={2}
title={this.props.content.get('dominantAttachment').get('title') ? this.props.content.get('dominantAttachment').get('title') : this.props.content.get('dominantAttachment').get('slug')}
subtitle={'type: '+this.props.content.get('dominantAttachment').get('attachment').get('type')}
actionIcon={
<IconButton className='secondary-button' onClick={this.props.handleRemoveDomMedia}>
<FontIcon className='mui-icons'>close</FontIcon>
</IconButton>
}
className='grid-tile'
>
<img src={this.props.content.get('dominantAttachment').get('attachment').get('public_url')} onClick={this.editAttachment.bind(this, linkLocation)} style={{cursor:'pointer'}} />
</GridTile>
</GridList>
</Paper>
)
: (
<RaisedButton
label='Dominant Media'
style={{'width':'100%'}}
onClick={this.props.handleOpenDomMedia}
icon={<FontIcon className='mui-icons'>perm_media</FontIcon>}
/>
)
}
</div>
);
}
}
class ContentRichEditorContainer extends React.Component {
constructor(props) {
super(props);
this.editorRef = React.createRef();
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
if (this.editorRef.current) {
this.editorRef.current.resetEditorContent(nextProps.content.get('content_raw') ? nextProps.content.get('content_raw') : nextProps.content.get('content'));
}
return true;
}
if (this.props.lastSaved != nextProps.lastSaved
|| this.props.contentLimbo != nextProps.contentLimbo) {
return true;
}
if (!Immutable.is(this.props.pendingDraft, nextProps.pendingDraft)) {
return true;
}
if (this.props.content.get('content_raw') != nextProps.content.get('content_raw')
|| this.props.content.get('content') != nextProps.content.get('content')
|| this.props.content.get('version') != nextProps.content.get('version')
|| this.props.content.get('uuid') != nextProps.content.get('uuid')
|| this.props.useSmartQuotes != nextProps.useSmartQuotes
|| this.props.useSuggestedLinks != nextProps.useSuggestedLinks
|| this.props.editor2 != nextProps.editor2) {
if (this.editorRef.current) {
this.editorRef.current.resetEditorContent(nextProps.content.get('content_raw'));
}
return true;
}
return false;
}
render() {
let useOriginal = this.props.editor2 == false;
if (this.props.content.get('content_raw') && this.props.content.get('content_raw').indexOf('entityMap') !== -1) {
useOriginal = true;
}
if (this.props.content.get('content_raw') && this.props.content.get('content_raw').indexOf('"object":"value"') !== -1) {
useOriginal = false;
}
return (
<div className='editor-container'>
{
useOriginal
? (
<RichEditor
label='Content'
name="content"
toolbarRightElement={
<div>
{
this.props.lastSaved
? (
<DraftLastAutosave lastsave={this.props.lastSaved} />
)
: (
<DraftRestore buttonLabel='Restore Draft' pendingDraft={this.props.pendingDraft} onRestore={this.props.handleRestoreDraft} />
)
}
</div>
}
onStateChange={this.props.handleStateChange}
readOnly={this.props.disabled}
enableComments={true}
version={this.props.content.get('version')}
defaultValue={(this.props.content.get('content_raw') && this.props.content.get('content_raw').length) ? this.props.content.get('content_raw') : this.props.content.get('content')}
onChange={this.props.handleEditorChange}
contentUuid={this.props.content.get('uuid') ? this.props.content.get('uuid') : false}
/>
)
: (
<RichEditor2
contentUuid={this.props.content.get('uuid') ? this.props.content.get('uuid') : false}
contentVersion={this.props.content.get('version') ? this.props.content.get('version') : 1}
onChange={this.props.handleEditorChange2}
initialValue={(this.props.content.get('content_raw') && this.props.content.get('content_raw').length) ? this.props.content.get('content_raw') : this.props.content.get('content')}
label='Content'
readOnly={this.props.disabled}
smartQuotes={this.props.useSmartQuotes}
related={this.props.useSuggestedLinks ? this.props.content.get('relatedContent') : null}
keywords={this.props.useSuggestedLinks ? this.props.content.get('keywords') : null}
ref={this.editorRef}
/>
)
}
<Divider />
{
this.props.contentLimbo
? (
<small>
<WordCounter count={this.props.contentLimbo} displayEmpty={true} />
</small>
)
: ''
}
{
useOriginal && this.props.editor2
? (
<React.Fragment>
<RaisedButton
label="Convert to RichEditor2"
disabled={!this.props.editor2}
onClick={this.props.convertContent}
style={{marginLeft:'10px'}}
/>
<IconButton
href='https://snworks.zendesk.com/hc/en-us/articles/360020387252'
target='help'
>
<FontIcon className="mui-icons">help</FontIcon>
</IconButton>
</React.Fragment>
)
: (
''
)
}
</div>
);
}
}
class ContentAbstractEditorContainer extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.disabled != nextProps.disabled) {
return true;
}
if (this.props.content.get('abstract_raw') != nextProps.content.get('abstract_raw')
|| this.props.content.get('abstract') != nextProps.content.get('abstract')) {
return true;
}
if (this.props.abstractLimbo != nextProps.abstractLimbo) {
return true;
}
return false;
}
render() {
let useVal;
try {
if (this.props.content.get('abstract_raw') && this.props.content.get('abstract_raw').length) {
JSON.parse(this.props.content.get('abstract_raw'));
}
useVal = (this.props.content.get('abstract_raw') && this.props.content.get('abstract_raw').length)
? this.props.content.get('abstract_raw')
: this.props.content.get('abstract');
} catch (e) {
useVal = this.props.content.get('abstract');
}
return (
<div className='editor-container'>
<RichEditor
label='Abstract'
name="abstract"
readOnly={this.props.disabled}
defaultValue={useVal}
onChange={this.props.handleEditorChange}
/>
<Divider />
{
this.props.abstractLimbo
? (
<small>
<WordCounter count={this.props.abstractLimbo} displayEmpty={true} />
</small>
)
: ''
}
</div>
);
}
}
class ContentRelatedBox extends React.Component
{
handleClick(item, e) {
browserHistory.push("/ceo/redirect?next=content/" + item.get('uuid'));
}
render() {
if (!this.props.content.get('relatedContent') || !this.props.content.get('relatedContent').size) {
return '';
}
return (
<div className='clear-bottom'>
<Paper>
<Subheader>Related Items</Subheader>
<div style={{maxHeight:'400px', overflow: 'auto'}} className='padded'>
{this.props.content.get('relatedContent').map((item) => (
<div style={{marginBottom:'10px'}} onClick={this.handleClick.bind(this, item)}>
<span className="link-on-hover">{item.get('title')}</span>
</div>
))}
</div>
</Paper>
</div>
)
}
}
export default connect(mapStateToProps)(withDraftable(ContentItem));