From 396cd01a6cdb9ea9a171fec1e07087bc851d517d Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 8 Dec 2016 13:04:28 +0530 Subject: [PATCH 1/2] Github #166, Messaging: unposted content alert -- Implemented the required behaviour with Discussions page. Took liberty to implement the same behaviour when user has unposted content and tries to change the thread from the left panel of thread list. --- src/components/Feed/NewPost.jsx | 27 ++++++--- src/projects/detail/Messages.jsx | 9 ++- .../detail/containers/MessagesContainer.js | 60 ++++++++++++++++++- 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/components/Feed/NewPost.jsx b/src/components/Feed/NewPost.jsx index 653e16c2e..e295b4716 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,27 @@ class NewPost extends React.Component { onEditorChange(editorState) { this.setState({editorState}) - this.onNewPostChange() + this.validateSubmitState() + if (this.props.onNewPostChange) { + this.props.onNewPostChange(this.refs.title.value, stateToMarkdown(editorState.getCurrentContent())) + } } - 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) { + this.props.onNewPostChange(this.refs.title.value, stateToMarkdown(editorState.getCurrentContent())) + } + } + render() { const {currentUser, titlePlaceholder, isCreating} = this.props const {editorState, canSubmit} = this.state @@ -191,7 +204,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/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/MessagesContainer.js b/src/projects/detail/containers/MessagesContainer.js index 83afdb8f6..0ec6f3ed7 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,27 @@ 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) + } + + componentDidMount() { + this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) + window.addEventListener('beforeunload', this.onLeave) } componentWillMount() { @@ -46,6 +62,24 @@ class MessagesView extends React.Component { this.init(nextProps) } + componentWillUnmount() { + 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 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 +171,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 +202,12 @@ class MessagesView extends React.Component { }) } + onNewPostChange(title, content) { + this.setState({ + newPost: {title, content} + }) + } + onNewMessageChange(content) { this.setState({ threads: this.state.threads.map((item) => { @@ -200,6 +253,7 @@ class MessagesView extends React.Component { !props.isLoading) -const EnhancedMessagesView = enhance(MessagesView) +const EnhancedMessagesView = withRouter(enhance(MessagesView)) class MessagesContainer extends React.Component { constructor(props) { From 6b3974be29e5cfe8b697ba90272a92ca927a44c5 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 8 Dec 2016 15:20:03 +0530 Subject: [PATCH 2/2] Github #166, Messaging: unposted content alert -- Implemented the required behaviour with Dashboard page. -- Tried to handle the warning of multiple leave hooks for the same route, however, the solution didn't work. Moving ahead as right now it is a warning and not causing any problem. May be we can launch a challenge to get it fixed. --- src/components/Feed/NewPost.jsx | 6 ++- src/projects/detail/Dashboard.jsx | 4 +- .../detail/containers/FeedContainer.js | 42 ++++++++++++++++++- .../detail/containers/MessagesContainer.js | 34 ++++++++++++++- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/components/Feed/NewPost.jsx b/src/components/Feed/NewPost.jsx index e295b4716..37d5982ce 100644 --- a/src/components/Feed/NewPost.jsx +++ b/src/components/Feed/NewPost.jsx @@ -128,7 +128,8 @@ class NewPost extends React.Component { this.setState({editorState}) this.validateSubmitState() if (this.props.onNewPostChange) { - this.props.onNewPostChange(this.refs.title.value, stateToMarkdown(editorState.getCurrentContent())) + // NOTE: uses getPlainText method to avoid newline character for empty content + this.props.onNewPostChange(this.refs.title.value, editorState.getCurrentContent().getPlainText()) } } @@ -143,7 +144,8 @@ class NewPost extends React.Component { const { editorState } = this.state this.validateSubmitState() if (this.props.onNewPostChange) { - this.props.onNewPostChange(this.refs.title.value, stateToMarkdown(editorState.getCurrentContent())) + // NOTE: uses getPlainText method to avoid newline character for empty content + this.props.onNewPostChange(this.refs.title.value, editorState.getCurrentContent().getPlainText()) } } 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/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 0ec6f3ed7..fc81d6c45 100644 --- a/src/projects/detail/containers/MessagesContainer.js +++ b/src/projects/detail/containers/MessagesContainer.js @@ -47,11 +47,14 @@ class MessagesView extends React.Component { 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() { - this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) + const routeLeaveHook = this.props.router.setRouteLeaveHook(this.props.route, this.onLeave) window.addEventListener('beforeunload', this.onLeave) + this.setState({ routeLeaveHook }) } componentWillMount() { @@ -64,6 +67,9 @@ class MessagesView extends React.Component { componentWillUnmount() { window.removeEventListener('beforeunload', this.onLeave) + if (this.state.routeLeaveHook) { + this.state.routeLeaveHook() + } } // Notify user if they navigate away while the form is modified. @@ -208,6 +214,30 @@ class MessagesView extends React.Component { }) } + 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) => { @@ -281,7 +311,7 @@ class MessagesView extends React.Component {
this.setState({isCreateNewMessage: true})} + onAdd={ this.onNewThreadClick } threads={threads} onSelect={this.onThreadSelect} showAddButton={ !!currentMemberRole }