diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx
index 70876b994..7c0b0ddb0 100644
--- a/src/components/Footer/Footer.jsx
+++ b/src/components/Footer/Footer.jsx
@@ -9,7 +9,7 @@ const Footer = () => {
const otherNavigationItems = [
{img: '', text: 'About', link: 'https://www.topcoder.com/about-topcoder/', target: '_blank'},
{img: '', text: 'Contact', link: 'https://www.topcoder.com/about-topcoder/contact/', target: '_blank'},
- {img: '', text: 'Help', link: 'https://help.topcoder.com/hc/en-us', target: '_blank'},
+ {img: '', text: 'Help', link: 'https://help.topcoder.com/hc/en-us/articles/225540188-Topcoder-Connect-FAQs', target: '_blank'},
{img: '', text: 'Privacy', link: 'https://www.topcoder.com/community/how-it-works/privacy-policy/', target: '_blank'},
{img: '', text: 'Terms', link: 'https://connect.topcoder.com/terms'}
]
diff --git a/src/components/FooterV2/FooterV2.jsx b/src/components/FooterV2/FooterV2.jsx
index 48ef8a26b..1fd5bb273 100644
--- a/src/components/FooterV2/FooterV2.jsx
+++ b/src/components/FooterV2/FooterV2.jsx
@@ -6,7 +6,7 @@ const FooterV2 = () => (
diff --git a/src/components/MessageList/MessageList.jsx b/src/components/MessageList/MessageList.jsx
index cbd89a0c1..cd85229ec 100644
--- a/src/components/MessageList/MessageList.jsx
+++ b/src/components/MessageList/MessageList.jsx
@@ -57,8 +57,9 @@ class MessageList extends Component {
componentDidMount() {
const { scrollPosition } = this.props
const panelMessages = this.refs.panelMessages
- // 145 = 60 for topbar + 45 for panel title + 20px for margin between topbar and left panel + 10px padding
- panelMessages.style.height = (window.innerHeight - 145) + 'px'
+ // 215 = 60 for topbar + 45 for panel title + 20px for margin between topbar and left panel + 10px padding
+ // + 60px footer + 10px margin bw footer and left panel
+ panelMessages.style.height = (window.innerHeight - 215) + 'px'
if (scrollPosition) {
// We use requestAnimationFrame because this function may be executed before
// the DOM elements are actually drawn.
diff --git a/src/components/TeamManagement/TeamManagement.scss b/src/components/TeamManagement/TeamManagement.scss
index d5ad39582..cbab61f3c 100644
--- a/src/components/TeamManagement/TeamManagement.scss
+++ b/src/components/TeamManagement/TeamManagement.scss
@@ -214,6 +214,10 @@ $tc-body-extra-small : 12px;
}
}
+ input::-ms-clear {
+ display: none;
+ }
+
.modal-inline-form{
display: flex;
margin-bottom: $base-unit*2;
diff --git a/src/components/TopBar/TopBar.jsx b/src/components/TopBar/TopBar.jsx
index 12e3779c2..3c73be10a 100644
--- a/src/components/TopBar/TopBar.jsx
+++ b/src/components/TopBar/TopBar.jsx
@@ -6,7 +6,7 @@ import cn from 'classnames'
import _ from 'lodash'
import { UserDropdown, Icons } from 'appirio-tech-react-components'
-const { ConnectLogoBeta } = Icons
+const { ConnectLogo } = Icons
import { SearchBar } from 'appirio-tech-react-components'
import Filters from './Filters'
import ProjectToolBar from './ProjectToolBar'
@@ -67,7 +67,7 @@ class TopBar extends Component {
]
const logo = (
-
+
)
const avatar = (
diff --git a/src/projects/detail/Dashboard.jsx b/src/projects/detail/Dashboard.jsx
index 45d89c546..277d41e96 100644
--- a/src/projects/detail/Dashboard.jsx
+++ b/src/projects/detail/Dashboard.jsx
@@ -1,11 +1,13 @@
import React from 'react'
+import { connect } from 'react-redux'
import ProjectInfoContainer from './containers/ProjectInfoContainer'
import FeedContainer from './containers/FeedContainer'
import Sticky from 'react-stickynode'
+import spinnerWhileLoading from '../../components/LoadingSpinner'
require('./Dashboard.scss')
-const Dashboard = ({project, currentMemberRole}) => (
+const DashboardView = ({project, currentMemberRole, route}) => (
@@ -16,10 +18,29 @@ const Dashboard = ({project, currentMemberRole}) => (
-
+
)
-export default Dashboard
+const enhance = spinnerWhileLoading(props => !props.isLoading)
+const EnhancedDashboardView = enhance(DashboardView)
+
+class Dashboard extends React.Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ return
+ }
+}
+
+const mapStateToProps = ({ projectState }) => {
+ return {
+ isLoading : projectState.isLoading
+ }
+}
+
+export default connect(mapStateToProps)(Dashboard)
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/Messages.scss b/src/projects/detail/Messages.scss
index be8e80b37..201dcdfa4 100644
--- a/src/projects/detail/Messages.scss
+++ b/src/projects/detail/Messages.scss
@@ -18,12 +18,13 @@
@include flexBox;
max-width: 1110px;
margin: 20px auto;
- height: calc(100% - 20px);// 20px is for bottom margin
+ height: calc(100% - 80px);// 20px is for bottom margin, 60px for footer
.left-area {
@include flexWidth(1);
max-width: 360px;
z-index: 14;/* Don't know the exact reason, but it needs explicit z-index to get behind the topbar*/
+ transform: translate3d(0px, 0px, 0px);
}
.right-area {
@include flexWidth(2);
diff --git a/src/projects/detail/containers/FeedContainer.js b/src/projects/detail/containers/FeedContainer.js
index e14298aaf..f621ca219 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,7 +57,28 @@ class FeedView extends React.Component {
this.init(nextProps)
}
- mapFeed(feed, showAll = false) {
+ 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, resetNewComment = false) {
const { allMembers } = this.props
const item = _.pick(feed, ['id', 'date', 'read', 'tag', 'title', 'totalPosts', 'userId', 'reference', 'referenceId', 'postIds', 'isAddingComment', 'isLoadingComments', 'error'])
if (isSystemUser(item.userId)) {
@@ -72,20 +103,27 @@ class FeedView extends React.Component {
author: isSystemUser(p.userId) ? SYSTEM_USER : allMembers[p.userId]
}
}
+ const validPost = (post) => {
+ return post.type === 'post' && (post.body && post.body.trim().length || !isSystemUser(post.userId))
+ }
if (showAll) {
// if we are showing all comments, just iterate through the entire array
_.forEach(_.slice(feed.posts, 1), p => {
- p.type === 'post' ? item.comments.push(_toComment(p)) : item.totalComments--
+ validPost(p) ? item.comments.push(_toComment(p)) : item.totalComments--
})
} else {
// otherwise iterate from right and add to the beginning of the array
_.forEachRight(_.slice(feed.posts, 1), (p) => {
- p.type === 'post' ? item.comments.unshift(_toComment(p)) : item.totalComments--
+ validPost(p) ? item.comments.unshift(_toComment(p)) : item.totalComments--
if (!feed.showAll && item.comments.length === THREAD_MESSAGES_PAGE_SIZE)
return false
})
}
item.newComment = ''
+ if (!resetNewComment) {
+ const feedFromState = _.find(this.state.feeds, f => feed.id === f.id)
+ item.newComment = feedFromState ? feedFromState.newComment : ''
+ }
item.hasMoreComments = item.comments.length !== item.totalComments
return item
}
@@ -99,6 +137,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 +238,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 +248,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..6b3455a84 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'
@@ -9,6 +10,7 @@ import NewPost from '../../../components/Feed/NewPost'
import { laodProjectMessages, createProjectTopic, loadFeedComments, addFeedComment } from '../../actions/projectTopics'
import spinnerWhileLoading from '../../../components/LoadingSpinner'
import {FullHeightContainer} from 'appirio-tech-react-components'
+import FooterV2 from '../../../components/FooterV2/FooterV2'
import {
THREAD_MESSAGES_PAGE_SIZE,
@@ -30,12 +32,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,7 +66,28 @@ class MessagesView extends React.Component {
this.init(nextProps)
}
- mapFeed(feed, isActive, showAll = false) {
+ 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, resetNewMessage = false) {
const { allMembers } = this.props
const item = _.pick(feed, ['id', 'date', 'read', 'tag', 'title', 'totalPosts', 'userId', 'reference', 'referenceId', 'postIds', 'isAddingComment', 'isLoadingComments', 'error'])
item.isActive = isActive
@@ -72,20 +113,27 @@ class MessagesView extends React.Component {
author: isSystemUser(p.userId) ? SYSTEM_USER : allMembers[p.userId]
}
}
+ const validPost = (post) => {
+ return post.type === 'post' && (post.body && post.body.trim().length || !isSystemUser(post.userId))
+ }
if (showAll) {
// if we are showing all comments, just iterate through the entire array
_.forEach(feed.posts, p => {
- p.type === 'post' ? item.messages.push(_toComment(p)) : item.totalComments--
+ validPost(p) ? item.messages.push(_toComment(p)) : item.totalComments--
})
} else {
// otherwise iterate from right and add to the beginning of the array
_.forEachRight(feed.posts, (p) => {
- p.type === 'post' ? item.messages.unshift(_toComment(p)) : item.totalComments--
+ validPost(p) ? item.messages.unshift(_toComment(p)) : item.totalComments--
if (!feed.showAll && item.messages.length === THREAD_MESSAGES_PAGE_SIZE)
return false
})
}
item.newMessage = ''
+ if (!resetNewMessage) {
+ const threadFromState = _.find(this.state.threads, t => feed.id === t.id)
+ item.newMessage = threadFromState ? threadFromState.newMessage : ''
+ }
item.hasMoreMessages = item.messages.length < item.totalComments
return item
}
@@ -137,15 +185,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 +216,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 +291,7 @@ class MessagesView extends React.Component {
this.setState({isCreateNewMessage: true})}
+ onAdd={ this.onNewThreadClick }
threads={threads}
onSelect={this.onThreadSelect}
showAddButton={ !!currentMemberRole }
showEmptyState={ showEmptyState && !threads.length }
scrollPosition={ scrollPosition }
/>
+
{ (showEmptyState && !threads.length) &&
@@ -251,7 +344,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) {
diff --git a/src/projects/detail/containers/ProjectInfoContainer.js b/src/projects/detail/containers/ProjectInfoContainer.js
index 36fce9b24..d4220c89b 100644
--- a/src/projects/detail/containers/ProjectInfoContainer.js
+++ b/src/projects/detail/containers/ProjectInfoContainer.js
@@ -135,7 +135,7 @@ class ProjectInfoContainer extends React.Component {
currentMemberRole={currentMemberRole}
description={project.description}
type={project.type}
- devices={project.details.devices || []}
+ devices={ _.get(project, 'details.devices', []) }
status={project.status} onChangeStatus={this.onChangeStatus}
duration={duration}
budget={budget}
diff --git a/src/projects/detail/containers/TeamManagementContainer.jsx b/src/projects/detail/containers/TeamManagementContainer.jsx
index 57383841c..edbd5c67e 100644
--- a/src/projects/detail/containers/TeamManagementContainer.jsx
+++ b/src/projects/detail/containers/TeamManagementContainer.jsx
@@ -3,7 +3,7 @@ import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import _ from 'lodash'
import {
- ROLE_CONNECT_COPILOT, ROLE_CONNECT_MANAGER,
+ ROLE_CONNECT_COPILOT, ROLE_CONNECT_MANAGER, ROLE_ADMINISTRATOR,
PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_CUSTOMER, AUTOCOMPLETE_TRIGGER_LENGTH
} from '../../../config/constants'
import TeamManagement from '../../../components/TeamManagement/TeamManagement'
@@ -221,13 +221,14 @@ class TeamManagementContainer extends Component {
}
const mapStateToProps = ({ loadUser, members }) => {
+ const powerUserRoles = [ROLE_CONNECT_COPILOT, ROLE_CONNECT_MANAGER, ROLE_ADMINISTRATOR]
+ const managerRoles = [ ROLE_ADMINISTRATOR, ROLE_CONNECT_MANAGER ]
return {
currentUser: {
userId: parseInt(loadUser.user.id),
isCopilot: _.indexOf(loadUser.user.roles, ROLE_CONNECT_COPILOT) > -1,
- isManager: _.indexOf(loadUser.user.roles, ROLE_CONNECT_MANAGER) > -1,
- isCustomer: _.indexOf(loadUser.user.roles, ROLE_CONNECT_MANAGER) === -1
- && _.indexOf(loadUser.user.roles, ROLE_CONNECT_COPILOT) === -1
+ isManager: loadUser.user.roles.some((role) => managerRoles.indexOf(role) !== -1),
+ isCustomer: !loadUser.user.roles.some((role) => powerUserRoles.indexOf(role) !== -1)
},
allMembers: _.values(members.members)
}
diff --git a/src/projects/reducers/project.js b/src/projects/reducers/project.js
index 1791b363f..e1523a823 100644
--- a/src/projects/reducers/project.js
+++ b/src/projects/reducers/project.js
@@ -9,7 +9,8 @@ import {
REMOVE_PROJECT_ATTACHMENT_PENDING, REMOVE_PROJECT_ATTACHMENT_SUCCESS, REMOVE_PROJECT_ATTACHMENT_FAILURE,
ADD_PROJECT_MEMBER_PENDING, ADD_PROJECT_MEMBER_SUCCESS, ADD_PROJECT_MEMBER_FAILURE,
UPDATE_PROJECT_MEMBER_PENDING, UPDATE_PROJECT_MEMBER_SUCCESS, UPDATE_PROJECT_MEMBER_FAILURE,
- REMOVE_PROJECT_MEMBER_PENDING, REMOVE_PROJECT_MEMBER_SUCCESS, REMOVE_PROJECT_MEMBER_FAILURE
+ REMOVE_PROJECT_MEMBER_PENDING, REMOVE_PROJECT_MEMBER_SUCCESS, REMOVE_PROJECT_MEMBER_FAILURE,
+ GET_PROJECTS_SUCCESS
} from '../../config/constants'
import _ from 'lodash'
import update from 'react-addons-update'
@@ -50,6 +51,7 @@ export const projectState = function (state=initialState, action) {
})
case CLEAR_LOADED_PROJECT:
+ case GET_PROJECTS_SUCCESS:
return Object.assign({}, state, {
project: {}
})
diff --git a/src/projects/reducers/projectTopics.js b/src/projects/reducers/projectTopics.js
index 27ee4ee96..c3fa25651 100644
--- a/src/projects/reducers/projectTopics.js
+++ b/src/projects/reducers/projectTopics.js
@@ -157,6 +157,7 @@ export const projectTopics = function (state=initialState, action) {
posts: { $push: payload.posts },
isLoadingComments: { $set : false }
})
+ updatedFeed.posts = _.sortBy(updatedFeed.posts, ['id'])
const feedUpdateQuery = {}
feedUpdateQuery[tag] = { topics: { $splice: [[feedIndex, 1, updatedFeed]] } }
// update the state
diff --git a/src/routes.jsx b/src/routes.jsx
index 67ebc1384..d30c7a702 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -17,6 +17,14 @@ const LoginRedirect = withProps({
redirectTo: `${ACCOUNTS_APP_LOGIN_URL}?retUrl=${window.location.protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}`
})(RedirectComponent)
+const redirectToConnect = (nextState, replace, callback) => {
+ if(window.location.hostname.indexOf('connectv2') === 0) {
+ window.location.assign(window.location.href.replace('connectv2', 'connect'))
+ return
+ }
+ callback()
+}
+
const redirectToProject = (nextState, replace, callback) => {
const feedId = nextState.params.feedId
getFreshToken().then(() => {
@@ -50,7 +58,7 @@ const redirectToProject = (nextState, replace, callback) => {
}
export default (
- window.scrollTo(0, 0)} component={ App }>
+ window.scrollTo(0, 0)} component={ App } onEnter={ redirectToConnect }>