diff --git a/src/components/Feed/NewPost.jsx b/src/components/Feed/NewPost.jsx index 653e16c2e..37d5982ce 100644 --- a/src/components/Feed/NewPost.jsx +++ b/src/components/Feed/NewPost.jsx @@ -41,12 +41,13 @@ class NewPost extends React.Component { constructor(props) { super(props) this.state = {editorState: EditorState.createEmpty(), expandedEditor: false, canSubmit: false} + this.onTitleChange = this.onTitleChange.bind(this) this.onEditorChange = this.onEditorChange.bind(this) this.handleKeyCommand = this.handleKeyCommand.bind(this) this.toggleBlockType = this.toggleBlockType.bind(this) this.toggleInlineStyle = this.toggleInlineStyle.bind(this) this.onClickOutside = this.onClickOutside.bind(this) - this.onNewPostChange = this.onNewPostChange.bind(this) + this.validateSubmitState = this.validateSubmitState.bind(this) } componentDidMount() { @@ -59,11 +60,11 @@ class NewPost extends React.Component { } componentWillReceiveProps(nextProps) { - if (!(nextProps.isCreating || nextProps.hasError && !nextProps.isCreating)) { + if (nextProps.isCreating !== this.props.isCreating && !nextProps.isCreating && !nextProps.hasError) { this.setState({editorState: EditorState.createEmpty()}) this.refs.title.value = '' } - this.onNewPostChange() + this.validateSubmitState() } onClickOutside(evt) { @@ -125,15 +126,29 @@ class NewPost extends React.Component { onEditorChange(editorState) { this.setState({editorState}) - this.onNewPostChange() + this.validateSubmitState() + if (this.props.onNewPostChange) { + // NOTE: uses getPlainText method to avoid newline character for empty content + this.props.onNewPostChange(this.refs.title.value, editorState.getCurrentContent().getPlainText()) + } } - onNewPostChange() { + validateSubmitState() { + const { editorState } = this.state this.setState({ - canSubmit: this.refs.title && !!this.refs.title.value.trim().length && this.state.editorState.getCurrentContent().hasText() + canSubmit: this.refs.title && !!this.refs.title.value.trim().length && editorState.getCurrentContent().hasText() }) } + onTitleChange() { + const { editorState } = this.state + this.validateSubmitState() + if (this.props.onNewPostChange) { + // NOTE: uses getPlainText method to avoid newline character for empty content + this.props.onNewPostChange(this.refs.title.value, editorState.getCurrentContent().getPlainText()) + } + } + render() { const {currentUser, titlePlaceholder, isCreating} = this.props const {editorState, canSubmit} = this.state @@ -191,7 +206,7 @@ class NewPost extends React.Component { ref="title" className="new-post-title" type="text" - onChange={this.onNewPostChange} + onChange={this.onTitleChange} placeholder={ titlePlaceholder || 'Title of the post'} />
diff --git a/src/projects/detail/Dashboard.jsx b/src/projects/detail/Dashboard.jsx index 45d89c546..1279d4015 100644 --- a/src/projects/detail/Dashboard.jsx +++ b/src/projects/detail/Dashboard.jsx @@ -5,7 +5,7 @@ import Sticky from 'react-stickynode' require('./Dashboard.scss') -const Dashboard = ({project, currentMemberRole}) => ( +const Dashboard = ({project, currentMemberRole, route}) => (
@@ -16,7 +16,7 @@ const Dashboard = ({project, currentMemberRole}) => (
- +
diff --git a/src/projects/detail/Messages.jsx b/src/projects/detail/Messages.jsx index 3086400fa..a1e9f5a2a 100644 --- a/src/projects/detail/Messages.jsx +++ b/src/projects/detail/Messages.jsx @@ -4,7 +4,12 @@ import MessagesContainer from './containers/MessagesContainer' require('./Messages.scss') -const Messages = ({ location, project, currentMemberRole }) => ( - +const Messages = ({ location, project, currentMemberRole, route }) => ( + ) export default Messages diff --git a/src/projects/detail/containers/FeedContainer.js b/src/projects/detail/containers/FeedContainer.js index e14298aaf..5a1e21bab 100644 --- a/src/projects/detail/containers/FeedContainer.js +++ b/src/projects/detail/containers/FeedContainer.js @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react' +import { withRouter } from 'react-router' import _ from 'lodash' import { THREAD_MESSAGES_PAGE_SIZE, @@ -36,7 +37,16 @@ class FeedView extends React.Component { this.onNewCommentChange = this.onNewCommentChange.bind(this) this.onShowAllComments = this.onShowAllComments.bind(this) this.onAddNewComment = this.onAddNewComment.bind(this) - this.state = { feeds : [], showAll: [] } + this.onLeave = this.onLeave.bind(this) + this.isChanged = this.isChanged.bind(this) + this.onNewPostChange = this.onNewPostChange.bind(this) + this.state = { feeds : [], showAll: [], newPost: {} } + } + + componentDidMount() { + const routeLeaveHook = this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) + window.addEventListener('beforeunload', this.onLeave) + this.setState({ routeLeaveHook }) } componentWillMount() { @@ -47,6 +57,27 @@ class FeedView extends React.Component { this.init(nextProps) } + componentWillUnmount() { + if (this.state.routeLeaveHook) { + this.state.routeLeaveHook() + } + window.removeEventListener('beforeunload', this.onLeave) + } + + // Notify user if they navigate away while the form is modified. + onLeave(e) { + if (this.isChanged()) { + return e.returnValue = 'You have uposted content. Are you sure you want to leave?' + } + } + + isChanged() { + const { newPost } = this.state + const hasComment = !_.isUndefined(_.find(this.state.feeds, (feed) => feed.newComment && feed.newComment.length)) + const hasThread = (newPost.title && !!newPost.title.trim().length) || ( newPost.content && !!newPost.content.trim().length) + return hasThread || hasComment + } + mapFeed(feed, showAll = false) { const { allMembers } = this.props const item = _.pick(feed, ['id', 'date', 'read', 'tag', 'title', 'totalPosts', 'userId', 'reference', 'referenceId', 'postIds', 'isAddingComment', 'isLoadingComments', 'error']) @@ -99,6 +130,12 @@ class FeedView extends React.Component { }) } + onNewPostChange(title, content) { + this.setState({ + newPost: {title, content} + }) + } + onNewPost({title, content}) { const { project } = this.props const newFeed = { @@ -194,6 +231,7 @@ class FeedView extends React.Component { isCreating={ isCreatingFeed } hasError={ error } heading="NEW STATUS POST" + onNewPostChange={this.onNewPostChange} titlePlaceholder="Share the latest project updates with the team" /> } @@ -203,7 +241,7 @@ class FeedView extends React.Component { } } const enhance = spinnerWhileLoading(props => !props.isLoading) -const EnhancedFeedView = enhance(FeedView) +const EnhancedFeedView = withRouter(enhance(FeedView)) class FeedContainer extends React.Component { diff --git a/src/projects/detail/containers/MessagesContainer.js b/src/projects/detail/containers/MessagesContainer.js index 83afdb8f6..fc81d6c45 100644 --- a/src/projects/detail/containers/MessagesContainer.js +++ b/src/projects/detail/containers/MessagesContainer.js @@ -1,5 +1,6 @@ import _ from 'lodash' import React from 'react' +import { withRouter } from 'react-router' import { connect } from 'react-redux' import update from 'react-addons-update' import MessageList from '../../../components/MessageList/MessageList' @@ -30,12 +31,30 @@ class MessagesView extends React.Component { constructor(props) { super(props) - this.state = { threads : [], activeThreadId : null, showEmptyState : true, showAll: []} + this.state = { + threads : [], + activeThreadId : null, + showEmptyState : true, + showAll: [], + newPost: {} + } this.onThreadSelect = this.onThreadSelect.bind(this) this.onShowAllComments = this.onShowAllComments.bind(this) this.onAddNewMessage = this.onAddNewMessage.bind(this) this.onNewMessageChange = this.onNewMessageChange.bind(this) this.onNewThread = this.onNewThread.bind(this) + this.onLeave = this.onLeave.bind(this) + this.isChanged = this.isChanged.bind(this) + this.onNewPostChange = this.onNewPostChange.bind(this) + this.changeThread = this.changeThread.bind(this) + this.onNewThreadClick = this.onNewThreadClick.bind(this) + this.showNewThreadForm = this.showNewThreadForm.bind(this) + } + + componentDidMount() { + const routeLeaveHook = this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) + window.addEventListener('beforeunload', this.onLeave) + this.setState({ routeLeaveHook }) } componentWillMount() { @@ -46,6 +65,27 @@ class MessagesView extends React.Component { this.init(nextProps) } + componentWillUnmount() { + window.removeEventListener('beforeunload', this.onLeave) + if (this.state.routeLeaveHook) { + this.state.routeLeaveHook() + } + } + + // Notify user if they navigate away while the form is modified. + onLeave(e) { + if (this.isChanged()) { + return e.returnValue = 'You have uposted content. Are you sure you want to leave?' + } + } + + isChanged() { + const { newPost } = this.state + const hasMessage = !_.isUndefined(_.find(this.state.threads, (thread) => thread.newMessage && thread.newMessage.length)) + const hasThread = (newPost.title && !!newPost.title.trim().length) || ( newPost.content && !!newPost.content.trim().length) + return hasThread || hasMessage + } + mapFeed(feed, isActive, showAll = false) { const { allMembers } = this.props const item = _.pick(feed, ['id', 'date', 'read', 'tag', 'title', 'totalPosts', 'userId', 'reference', 'referenceId', 'postIds', 'isAddingComment', 'isLoadingComments', 'error']) @@ -137,15 +177,28 @@ class MessagesView extends React.Component { } onThreadSelect(thread) { + const unsavedContentMsg = this.onLeave({}) + if (unsavedContentMsg) { + const changeConfirmed = confirm(unsavedContentMsg) + if (changeConfirmed) { + this.changeThread(thread) + } + } else { + this.changeThread(thread) + } + } + + changeThread(thread) { this.setState({ isCreateNewMessage: false, + newPost: {}, activeThreadId: thread.id, threads: this.state.threads.map((item) => { if (item.isActive) { if (item.id === thread.id) { return item } - return {...item, isActive: false, messages: item.messages.map((msg) => ({...msg, unread: false}))} + return {...item, isActive: false, newMessage: '', messages: item.messages.map((msg) => ({...msg, unread: false}))} } if (item.id === thread.id) { return {...item, isActive: true, unreadCount: 0} @@ -155,6 +208,36 @@ class MessagesView extends React.Component { }) } + onNewPostChange(title, content) { + this.setState({ + newPost: {title, content} + }) + } + + onNewThreadClick() { + const unsavedContentMsg = this.onLeave({}) + if (unsavedContentMsg) { + const changeConfirmed = confirm(unsavedContentMsg) + if (changeConfirmed) { + this.showNewThreadForm() + } + } else { + this.showNewThreadForm() + } + } + + showNewThreadForm() { + this.setState({ + isCreateNewMessage: true, + threads: this.state.threads.map((item) => { + if (item.isActive) { + return {...item, newMessage: ''} + } + return item + }) + }) + } + onNewMessageChange(content) { this.setState({ threads: this.state.threads.map((item) => { @@ -200,6 +283,7 @@ class MessagesView extends React.Component {
this.setState({isCreateNewMessage: true})} + onAdd={ this.onNewThreadClick } threads={threads} onSelect={this.onThreadSelect} showAddButton={ !!currentMemberRole } @@ -251,7 +335,7 @@ class MessagesView extends React.Component { } const enhance = spinnerWhileLoading(props => !props.isLoading) -const EnhancedMessagesView = enhance(MessagesView) +const EnhancedMessagesView = withRouter(enhance(MessagesView)) class MessagesContainer extends React.Component { constructor(props) {