diff --git a/.circleci/config.yml b/.circleci/config.yml
index 105dcca28..49fc017a3 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -136,7 +136,7 @@ workflows:
- build-dev
filters:
branches:
- only: ['feature/faqs']
+ only: ['feature/faqs', 'feature/project_september']
- deployProd:
context : org-global
diff --git a/docs/permissions.html b/docs/permissions.html
index 923e00b4e..65e337cbd 100644
--- a/docs/permissions.html
+++ b/docs/permissions.html
@@ -82,7 +82,6 @@
Invite new members or delete them. There are some additional restrictions for some roles.
View Member Suggestions
@@ -251,6 +346,7 @@ My Projects Filter
administrator
Connect Admin
Connect Manager
+ Connect Copilot Manager
Connect Account Manager
Business Development Representative
Presales
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-bold.woff b/src/assets/fonts/roboto-condensed/roboto-condensed-bold.woff
index f39ae79da..5a884ac8d 100644
Binary files a/src/assets/fonts/roboto-condensed/roboto-condensed-bold.woff and b/src/assets/fonts/roboto-condensed/roboto-condensed-bold.woff differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-bold.woff2 b/src/assets/fonts/roboto-condensed/roboto-condensed-bold.woff2
new file mode 100644
index 000000000..a7be05ead
Binary files /dev/null and b/src/assets/fonts/roboto-condensed/roboto-condensed-bold.woff2 differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-bolditalic.woff b/src/assets/fonts/roboto-condensed/roboto-condensed-bolditalic.woff
index 018b8cd69..54ea7d32e 100644
Binary files a/src/assets/fonts/roboto-condensed/roboto-condensed-bolditalic.woff and b/src/assets/fonts/roboto-condensed/roboto-condensed-bolditalic.woff differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-bolditalic.woff2 b/src/assets/fonts/roboto-condensed/roboto-condensed-bolditalic.woff2
new file mode 100644
index 000000000..ab07b77fc
Binary files /dev/null and b/src/assets/fonts/roboto-condensed/roboto-condensed-bolditalic.woff2 differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-italic.woff b/src/assets/fonts/roboto-condensed/roboto-condensed-italic.woff
index f3f434194..b1e9f87a1 100644
Binary files a/src/assets/fonts/roboto-condensed/roboto-condensed-italic.woff and b/src/assets/fonts/roboto-condensed/roboto-condensed-italic.woff differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-italic.woff2 b/src/assets/fonts/roboto-condensed/roboto-condensed-italic.woff2
new file mode 100644
index 000000000..29c94f1f5
Binary files /dev/null and b/src/assets/fonts/roboto-condensed/roboto-condensed-italic.woff2 differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-light.woff b/src/assets/fonts/roboto-condensed/roboto-condensed-light.woff
index 75187a9ff..b35a73134 100644
Binary files a/src/assets/fonts/roboto-condensed/roboto-condensed-light.woff and b/src/assets/fonts/roboto-condensed/roboto-condensed-light.woff differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-light.woff2 b/src/assets/fonts/roboto-condensed/roboto-condensed-light.woff2
new file mode 100644
index 000000000..443204ec9
Binary files /dev/null and b/src/assets/fonts/roboto-condensed/roboto-condensed-light.woff2 differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-lightitalic.woff b/src/assets/fonts/roboto-condensed/roboto-condensed-lightitalic.woff
index 454c3eec8..8cad339da 100644
Binary files a/src/assets/fonts/roboto-condensed/roboto-condensed-lightitalic.woff and b/src/assets/fonts/roboto-condensed/roboto-condensed-lightitalic.woff differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-lightitalic.woff2 b/src/assets/fonts/roboto-condensed/roboto-condensed-lightitalic.woff2
new file mode 100644
index 000000000..7055deb0e
Binary files /dev/null and b/src/assets/fonts/roboto-condensed/roboto-condensed-lightitalic.woff2 differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-regular.woff b/src/assets/fonts/roboto-condensed/roboto-condensed-regular.woff
index 2943f336b..fb76d7e4a 100644
Binary files a/src/assets/fonts/roboto-condensed/roboto-condensed-regular.woff and b/src/assets/fonts/roboto-condensed/roboto-condensed-regular.woff differ
diff --git a/src/assets/fonts/roboto-condensed/roboto-condensed-regular.woff2 b/src/assets/fonts/roboto-condensed/roboto-condensed-regular.woff2
new file mode 100644
index 000000000..6646a2fe3
Binary files /dev/null and b/src/assets/fonts/roboto-condensed/roboto-condensed-regular.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-black.woff b/src/assets/fonts/roboto/roboto-black.woff
old mode 100755
new mode 100644
index 022908657..ef0922a10
Binary files a/src/assets/fonts/roboto/roboto-black.woff and b/src/assets/fonts/roboto/roboto-black.woff differ
diff --git a/src/assets/fonts/roboto/roboto-black.woff2 b/src/assets/fonts/roboto/roboto-black.woff2
new file mode 100644
index 000000000..0a56238b7
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-black.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-blackitalic.woff b/src/assets/fonts/roboto/roboto-blackitalic.woff
old mode 100755
new mode 100644
index 1875c0b95..a71c40948
Binary files a/src/assets/fonts/roboto/roboto-blackitalic.woff and b/src/assets/fonts/roboto/roboto-blackitalic.woff differ
diff --git a/src/assets/fonts/roboto/roboto-blackitalic.woff2 b/src/assets/fonts/roboto/roboto-blackitalic.woff2
new file mode 100644
index 000000000..73b27094b
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-blackitalic.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-bold.woff b/src/assets/fonts/roboto/roboto-bold.woff
old mode 100755
new mode 100644
index 0c6994871..ddefa4d9c
Binary files a/src/assets/fonts/roboto/roboto-bold.woff and b/src/assets/fonts/roboto/roboto-bold.woff differ
diff --git a/src/assets/fonts/roboto/roboto-bold.woff2 b/src/assets/fonts/roboto/roboto-bold.woff2
new file mode 100644
index 000000000..1ba9cc610
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-bold.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-bolditalic.woff b/src/assets/fonts/roboto/roboto-bolditalic.woff
old mode 100755
new mode 100644
index 99de61af5..63f269e7f
Binary files a/src/assets/fonts/roboto/roboto-bolditalic.woff and b/src/assets/fonts/roboto/roboto-bolditalic.woff differ
diff --git a/src/assets/fonts/roboto/roboto-bolditalic.woff2 b/src/assets/fonts/roboto/roboto-bolditalic.woff2
new file mode 100644
index 000000000..bfcebb068
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-bolditalic.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-italic.woff b/src/assets/fonts/roboto/roboto-italic.woff
old mode 100755
new mode 100644
index dd7424438..f870cc014
Binary files a/src/assets/fonts/roboto/roboto-italic.woff and b/src/assets/fonts/roboto/roboto-italic.woff differ
diff --git a/src/assets/fonts/roboto/roboto-italic.woff2 b/src/assets/fonts/roboto/roboto-italic.woff2
new file mode 100644
index 000000000..8fac91116
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-italic.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-light.woff b/src/assets/fonts/roboto/roboto-light.woff
old mode 100755
new mode 100644
index cc534a381..ec99e7048
Binary files a/src/assets/fonts/roboto/roboto-light.woff and b/src/assets/fonts/roboto/roboto-light.woff differ
diff --git a/src/assets/fonts/roboto/roboto-light.woff2 b/src/assets/fonts/roboto/roboto-light.woff2
new file mode 100644
index 000000000..805734d6f
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-light.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-lightitalic.woff b/src/assets/fonts/roboto/roboto-lightitalic.woff
old mode 100755
new mode 100644
index 3071ff4f2..135880d4a
Binary files a/src/assets/fonts/roboto/roboto-lightitalic.woff and b/src/assets/fonts/roboto/roboto-lightitalic.woff differ
diff --git a/src/assets/fonts/roboto/roboto-lightitalic.woff2 b/src/assets/fonts/roboto/roboto-lightitalic.woff2
new file mode 100644
index 000000000..1f1752b57
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-lightitalic.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-medium.woff b/src/assets/fonts/roboto/roboto-medium.woff
old mode 100755
new mode 100644
index cd810ef92..73ad26a49
Binary files a/src/assets/fonts/roboto/roboto-medium.woff and b/src/assets/fonts/roboto/roboto-medium.woff differ
diff --git a/src/assets/fonts/roboto/roboto-medium.woff2 b/src/assets/fonts/roboto/roboto-medium.woff2
new file mode 100644
index 000000000..760a31028
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-medium.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-mediumitalic.woff b/src/assets/fonts/roboto/roboto-mediumitalic.woff
old mode 100755
new mode 100644
index 69a145801..45a3b8ee8
Binary files a/src/assets/fonts/roboto/roboto-mediumitalic.woff and b/src/assets/fonts/roboto/roboto-mediumitalic.woff differ
diff --git a/src/assets/fonts/roboto/roboto-mediumitalic.woff2 b/src/assets/fonts/roboto/roboto-mediumitalic.woff2
new file mode 100644
index 000000000..31c375645
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-mediumitalic.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-regular.woff b/src/assets/fonts/roboto/roboto-regular.woff
old mode 100755
new mode 100644
index bfa05d53f..9bee8027c
Binary files a/src/assets/fonts/roboto/roboto-regular.woff and b/src/assets/fonts/roboto/roboto-regular.woff differ
diff --git a/src/assets/fonts/roboto/roboto-regular.woff2 b/src/assets/fonts/roboto/roboto-regular.woff2
new file mode 100644
index 000000000..81fbd6ff2
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-regular.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-thin.woff b/src/assets/fonts/roboto/roboto-thin.woff
old mode 100755
new mode 100644
index f10b831e8..a957e7f90
Binary files a/src/assets/fonts/roboto/roboto-thin.woff and b/src/assets/fonts/roboto/roboto-thin.woff differ
diff --git a/src/assets/fonts/roboto/roboto-thin.woff2 b/src/assets/fonts/roboto/roboto-thin.woff2
new file mode 100644
index 000000000..05a1d210c
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-thin.woff2 differ
diff --git a/src/assets/fonts/roboto/roboto-thinitalic.woff b/src/assets/fonts/roboto/roboto-thinitalic.woff
old mode 100755
new mode 100644
index 9ef17a868..19ec1d57a
Binary files a/src/assets/fonts/roboto/roboto-thinitalic.woff and b/src/assets/fonts/roboto/roboto-thinitalic.woff differ
diff --git a/src/assets/fonts/roboto/roboto-thinitalic.woff2 b/src/assets/fonts/roboto/roboto-thinitalic.woff2
new file mode 100644
index 000000000..0f263824e
Binary files /dev/null and b/src/assets/fonts/roboto/roboto-thinitalic.woff2 differ
diff --git a/src/components/ActionCard/AddComment.jsx b/src/components/ActionCard/AddComment.jsx
index 72fbab580..0a02c49d5 100644
--- a/src/components/ActionCard/AddComment.jsx
+++ b/src/components/ActionCard/AddComment.jsx
@@ -25,7 +25,7 @@ export default class AddComment extends React.Component {
}
render() {
- const { className, avatarUrl, authorName, placeholder, isAdding, hasError, allMembers, projectMembers } = this.props
+ const { className, avatarUrl, authorName, placeholder, isAdding, hasError, allMembers, projectMembers, defaultContent } = this.props
return (
)
}
@@ -57,5 +58,9 @@ AddComment.propTypes = {
hasError: PropTypes.bool,
isAdding: PropTypes.bool,
allMembers: PropTypes.object,
- projectMembers: PropTypes.object
+ projectMembers: PropTypes.object,
+ /**
+ * Default value for comment content
+ */
+ defaultContent: PropTypes.string,
}
diff --git a/src/components/Feed/Feed.jsx b/src/components/Feed/Feed.jsx
index feeb4e4cd..9eec7834c 100644
--- a/src/components/Feed/Feed.jsx
+++ b/src/components/Feed/Feed.jsx
@@ -104,7 +104,7 @@ class Feed extends React.Component {
allowComments, comments, children, onNewCommentChange, onAddNewComment, isAddingComment, onSaveMessageChange,
onEditMessage, onSaveMessage, isSavingTopic, onDeleteMessage, onDeleteTopic, isDeletingTopic, error, allMembers,
onEnterFullscreenClick, onExitFullscreenClick, isFullScreen, commentId, projectMembers, commentAnchorPrefix, tag,
- inTopicDrawer, onDrawerClose
+ inTopicDrawer, onDrawerClose, newComment
} = this.props
const { editTopicMode, headerHeight } = this.state
@@ -222,6 +222,7 @@ class Feed extends React.Component {
commentId={commentId}
error={error}
commentAnchorPrefix={commentAnchorPrefix}
+ newComment={newComment}
/>
{children}
{isDeletingTopic &&
diff --git a/src/components/Feed/FeedComments.jsx b/src/components/Feed/FeedComments.jsx
index 0c16e242c..6cbf2a63d 100644
--- a/src/components/Feed/FeedComments.jsx
+++ b/src/components/Feed/FeedComments.jsx
@@ -180,7 +180,7 @@ class FeedComments extends React.Component {
const {
currentUser, onLoadMoreComments, isLoadingComments, hasMoreComments, onAddNewComment,
onNewCommentChange, error, avatarUrl, isAddingComment, allowComments, onSaveMessage, onDeleteMessage, allMembers,
- totalComments, isFullScreen, headerHeight, projectMembers, commentAnchorPrefix
+ totalComments, isFullScreen, headerHeight, projectMembers, commentAnchorPrefix, newComment
} = this.props
let { comments } = this.props
comments = _.sortBy(comments, 'createdBy')
@@ -383,6 +383,7 @@ class FeedComments extends React.Component {
hasError={error}
allMembers={allMembers}
projectMembers={projectMembers}
+ defaultContent={newComment}
/>
}
diff --git a/src/components/FooterNeedHelp/FooterNeedHelp.jsx b/src/components/FooterNeedHelp/FooterNeedHelp.jsx
new file mode 100644
index 000000000..2ad2548a6
--- /dev/null
+++ b/src/components/FooterNeedHelp/FooterNeedHelp.jsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import './FooterNeedHelp.scss'
+
+const FooterNeedHelp = () => (
+
+ Have an urgent issue?
+ E: support@topcoder.com
+
+)
+
+export default FooterNeedHelp
diff --git a/src/components/FooterNeedHelp/FooterNeedHelp.scss b/src/components/FooterNeedHelp/FooterNeedHelp.scss
new file mode 100644
index 000000000..5f620860e
--- /dev/null
+++ b/src/components/FooterNeedHelp/FooterNeedHelp.scss
@@ -0,0 +1,15 @@
+@import '~tc-ui/src/styles/tc-includes';
+
+.need-help {
+ @include roboto;
+ color: $tc-gray-50;
+ font-size: 12px;
+ line-height: 150%;
+ margin-top: 2 * $base-unit;
+ padding-bottom: 8 * $base-unit;
+ text-align: center;
+
+ a {
+ color: $tc-dark-blue-110;
+ }
+}
diff --git a/src/components/IncomepleteUserProfileDialog/IncompleteUserProfileDialog.jsx b/src/components/IncomepleteUserProfileDialog/IncompleteUserProfileDialog.jsx
new file mode 100644
index 000000000..625ff994b
--- /dev/null
+++ b/src/components/IncomepleteUserProfileDialog/IncompleteUserProfileDialog.jsx
@@ -0,0 +1,54 @@
+/**
+ * Dialog which shows incomplete user profile.
+ */
+import React from 'react'
+import PT from 'prop-types'
+import Modal from 'react-modal'
+import IncompleteUserProfile from '../IncompleteUserProfile/IncompleteUserProfile'
+import XMarkIcon from '../../assets/icons/icon-x-mark.svg'
+import styles from './IncompleteUserProfileDialog.scss'
+import LoadingIndicator from '../LoadingIndicator/LoadingIndicator'
+
+const IncompleteUserProfileDialog = ({
+ onCloseDialog,
+ title,
+ ...restProps,
+}) => {
+ return (
+
+
+
+
{title}
+
Complete your profile now.
+
+
+
+
+ {restProps.profileSettings.pending &&
}
+
+
+
+
+ )
+}
+
+IncompleteUserProfileDialog.propTypes = {
+ profileSettings: PT.object.isRequired,
+ saveProfileSettings: PT.func.isRequired,
+ isTopcoderUser: PT.bool.isRequired,
+ user: PT.object.isRequired,
+ onCloseDialog: PT.func.isRequired,
+ title: PT.string.isRequired,
+}
+
+export default IncompleteUserProfileDialog
diff --git a/src/components/IncomepleteUserProfileDialog/IncompleteUserProfileDialog.scss b/src/components/IncomepleteUserProfileDialog/IncompleteUserProfileDialog.scss
new file mode 100644
index 000000000..18e835784
--- /dev/null
+++ b/src/components/IncomepleteUserProfileDialog/IncompleteUserProfileDialog.scss
@@ -0,0 +1,66 @@
+@import '~tc-ui/src/styles/tc-includes';
+@import '../../styles/includes';
+
+:global(.management-dialog-overlay .project-dialog-conatiner .project-dialog) {
+ &.dialog {
+ width: 800px;
+ }
+
+ :global(.dialog-body).body {
+ max-height: 500px;
+ padding-top: $base-unit * 4;
+ position: relative;
+ }
+}
+
+:global(.incomplete-profile-dialog-overlay.management-dialog-overlay .project-dialog-conatiner .project-dialog .input-container) {
+ display: block;
+ background: transparent;
+ border-top: 0;
+ border-radius: 0;
+ margin: 0;
+ padding: 0;
+
+ input {
+ margin: 0;
+ }
+
+ :global(.dropdown-wrap) {
+ margin: 0;
+ width: 100px;
+ }
+}
+
+.subtitle {
+ padding-top: $base-unit * 4;
+ text-align: center;
+}
+
+.loadingOverlay {
+ align-items: center;
+ background-color: #fff;
+ display: flex;
+ left: 0;
+ height: 100%;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ z-index: 1;
+}
+
+@media screen and (max-width: $screen-md - 1px) {
+ :global(.management-dialog-overlay .project-dialog-conatiner .project-dialog) {
+ &.dialog {
+ width: 100%;
+ }
+ }
+}
+
+@media screen and (max-height: 700px) {
+ :global(.management-dialog-overlay .project-dialog-conatiner .project-dialog) {
+ :global(.dialog-body).body {
+ max-height: calc(100vh - 200px);
+ }
+ }
+}
diff --git a/src/components/IncompleteUserProfile/IncompleteUserProfile.jsx b/src/components/IncompleteUserProfile/IncompleteUserProfile.jsx
new file mode 100644
index 000000000..bdf948102
--- /dev/null
+++ b/src/components/IncompleteUserProfile/IncompleteUserProfile.jsx
@@ -0,0 +1,78 @@
+/**
+ * Incomplete User Profile Form.
+ */
+import React from 'react'
+import PT from 'prop-types'
+import { PROFILE_FIELDS_CONFIG } from '../../config/constants'
+import ProfileSettingsForm from '../../routes/settings/routes/profile/components/ProfileSettingsForm'
+import { getDefaultTopcoderRole } from '../../helpers/permissions'
+import { timezones } from 'appirio-tech-react-components/constants/timezones'
+
+const IncompleteUserProfile = ({
+ profileSettings,
+ saveProfileSettings,
+ isTopcoderUser,
+ user,
+ ...restProps
+}) => {
+ const fieldsConfig = isTopcoderUser ? PROFILE_FIELDS_CONFIG.TOPCODER : PROFILE_FIELDS_CONFIG.CUSTOMER
+ // never show avatar
+ delete fieldsConfig.avatar
+ // config the form to only show required fields which doesn't have the value yet
+ const missingFieldsConfig = _.reduce(fieldsConfig, (acc, isFieldRequired, fieldKey) => {
+ if (isFieldRequired && !_.get(profileSettings, `settings.${fieldKey}`)) {
+ acc[fieldKey] = isFieldRequired
+ }
+ return acc
+ }, {})
+
+ // prefill some fields of the profile
+ const prefilledProfileSettings = _.cloneDeep(profileSettings)
+
+ // if time zone is required and doesn't have a value yet,
+ // then detect timezone using browser feature and prefill the form
+ if (fieldsConfig.timeZone && !profileSettings.settings.timeZone) {
+ const autodetectedTimeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
+ // only use autodetected timezone if it's on our timezone list, otherwise leave it empty
+ if (_.find(timezones, { zoneName: autodetectedTimeZone})) {
+ prefilledProfileSettings.settings.timeZone = autodetectedTimeZone
+ }
+ console.log('Auto-detected timezone', prefilledProfileSettings.settings.timeZone)
+ }
+
+ if (isTopcoderUser) {
+ // We don't ask Topcoder User for "Company Name" and "Title"
+ // but server requires them, so if they are not yet defined, we set them automatically
+ if (!profileSettings.settings.companyName) {
+ prefilledProfileSettings.settings.companyName = 'Topcoder'
+ }
+ if (!profileSettings.settings.title) {
+ prefilledProfileSettings.settings.title = getDefaultTopcoderRole(user)
+ }
+ } else {
+ // at the moment we don't let users to update their business email, so in case it's not set, use registration email
+ if (!profileSettings.settings.businessEmail) {
+ prefilledProfileSettings.settings.businessEmail = user.email
+ }
+ }
+
+ return (
+
+ )
+}
+
+IncompleteUserProfile.propTypes = {
+ profileSettings: PT.object.isRequired,
+ saveProfileSettings: PT.func.isRequired,
+ isTopcoderUser: PT.bool.isRequired,
+ user: PT.object.isRequired,
+}
+
+export default IncompleteUserProfile
diff --git a/src/components/TeamManagement/CopilotManagementDialog.js b/src/components/TeamManagement/CopilotManagementDialog.js
new file mode 100644
index 000000000..b76cb11a6
--- /dev/null
+++ b/src/components/TeamManagement/CopilotManagementDialog.js
@@ -0,0 +1,238 @@
+import _ from 'lodash'
+import React from 'react'
+import PT from 'prop-types'
+import moment from 'moment'
+import Modal from 'react-modal'
+import XMarkIcon from '../../assets/icons/icon-x-mark.svg'
+import Avatar from 'appirio-tech-react-components/components/Avatar/Avatar'
+import PERMISSIONS from '../../config/permissions'
+
+import { hasPermission } from '../../helpers/permissions'
+import {getAvatarResized, getFullNameWithFallback} from '../../helpers/tcHelpers'
+import { compareEmail, compareHandles } from '../../helpers/utils'
+import AutocompleteInputContainer from './AutocompleteInputContainer'
+
+class ProjectManagementDialog extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ showAlreadyMemberError: false,
+ errorMessage: null
+ }
+ this.onChange = this.onChange.bind(this)
+ this.showIndividualErrors = this.showIndividualErrors.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { processingInvites, selectedMembers } = this.props
+
+ if (processingInvites && !nextProps.processingInvites ) {
+ const notInvitedSelectedMembers = _.reject(selectedMembers, (selectedMember) => (
+ this.isSelectedMemberAlreadyInvited(nextProps.copilotTeamInvites, selectedMember)
+ ))
+
+ this.props.onSelectedMembersUpdate(notInvitedSelectedMembers)
+
+ if (nextProps.error) {
+ this.showIndividualErrors(nextProps.error, notInvitedSelectedMembers)
+ }
+ }
+ }
+
+ onChange(selectedMembers) {
+ const { projectTeamInvites, members, topcoderTeamInvites, copilotTeamInvites } = this.props
+
+ const present = _.some(selectedMembers, (selectedMember) => (
+ this.isSelectedMemberAlreadyInvited(members, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(topcoderTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(projectTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(copilotTeamInvites, selectedMember)
+ ))
+
+ this.setState({
+ validUserText: !present,
+ showAlreadyMemberError: present,
+ errorMessage: null,
+ })
+
+ this.props.onSelectedMembersUpdate(selectedMembers)
+ }
+
+ isSelectedMemberAlreadyInvited(copilotTeamInvites = [], selectedMember) {
+ return !!copilotTeamInvites.find((invite) => (
+ (invite.email && compareEmail(invite.email, selectedMember.label)) ||
+ (invite.userId && compareHandles(invite.handle, selectedMember.label))
+ ))
+ }
+
+ showIndividualErrors(error) {
+ const uniqueMessages = _.groupBy(error.failed, 'message')
+
+ const msgs = _.keys(uniqueMessages).map((message) => {
+ const users = uniqueMessages[message].map((failed) => (
+ failed.email ? failed.email : failed.handle
+ ))
+
+ return ({
+ message,
+ users,
+ })
+ })
+
+ const listMessages = msgs.map((m) => `${m.users.join(', ')}: ${m.message}`)
+
+ this.setState({
+ errorMessage: listMessages.length > 0 ? listMessages.join('\n') : null
+ })
+ }
+
+ render() {
+ const {
+ members, currentUser, isMember, removeMember, removeInvite,
+ onCancel, copilotTeamInvites = [], selectedMembers, processingInvites,
+ } = this.props
+ const showRemove = currentUser.isAdmin || (!currentUser.isCopilot && isMember)
+ const showSuggestions = hasPermission(PERMISSIONS.SEE_MEMBER_SUGGESTIONS)
+ let i = 0
+ return (
+
+
+
+
+ Copilots
+
+
+
+
+ {(members.map((member) => {
+ if (!member.isCopilot) {
+ return null
+ }
+ i++
+ const remove = () => {
+ removeMember(member)
+ }
+ const userFullName = getFullNameWithFallback(member)
+ return (
+
+
+
+
+
+ {userFullName}
+
+ @{member.handle || 'ConnectUser'}
+
+
+
+ {showRemove &&
+ {(currentUser.userId === member.userId) ? 'Leave' : 'Remove'}
+
}
+
+ )
+ }))}
+ {(copilotTeamInvites.map((invite) => {
+ const remove = () => {
+ removeInvite(invite)
+ }
+ i++
+ const hasUserId = !_.isNil(invite.userId)
+ const handle = invite.handle
+ const userFullName = getFullNameWithFallback(invite)
+ return (
+
+
+
+ {hasUserId && {userFullName} }
+
+ {hasUserId && handle && @{handle} }
+ { (!hasUserId) && {invite.email} }
+
+
+ {showRemove &&
+ Remove
+
+ Invited {moment(invite.createdAt).format('MMM D, YY')}
+
+
}
+
+ )
+ }))}
+
+
+
+
invite more copilots
+
+ {this.state.showAlreadyMemberError &&
+ Project Member(s) can't be invited again. Please remove them from list.
+
}
+ { this.state.errorMessage &&
+ {this.state.errorMessage}
+
}
+
+ Send Invite
+
+
+
+
+
+ )
+ }
+}
+
+ProjectManagementDialog.defaultProps = {
+ projectTeamInvites: [],
+ topcoderTeamInvites: [],
+ members: []
+}
+
+ProjectManagementDialog.propTypes = {
+ error: PT.oneOfType([PT.object, PT.bool]),
+ currentUser: PT.object.isRequired,
+ members: PT.arrayOf(PT.object).isRequired,
+ allMembers: PT.arrayOf(PT.object).isRequired,
+ isMember: PT.bool.isRequired,
+ onCancel: PT.func.isRequired,
+ removeMember: PT.func.isRequired,
+ projectTeamInvites: PT.arrayOf(PT.object),
+ topcoderTeamInvites: PT.arrayOf(PT.object),
+ sendInvite: PT.func.isRequired,
+ removeInvite: PT.func.isRequired,
+ onSelectedMembersUpdate: PT.func.isRequired,
+ selectedMembers: PT.arrayOf(PT.object),
+ processingInvites: PT.bool.isRequired,
+}
+
+export default ProjectManagementDialog
diff --git a/src/components/TeamManagement/Dialog.js b/src/components/TeamManagement/Dialog.js
index 809da1c1f..31a2b1ec5 100644
--- a/src/components/TeamManagement/Dialog.js
+++ b/src/components/TeamManagement/Dialog.js
@@ -1,114 +1,47 @@
import React from 'react'
import Modal from 'react-modal'
import PT from 'prop-types'
-import SelectDropdown from '../SelectDropdown/SelectDropdown'
import LoadingIndicator from '../../components/LoadingIndicator/LoadingIndicator'
-class Dialog extends React.Component {
-
- constructor(props) {
- super(props)
- this.state = {
- role: 'manager'
- }
-
- this.roles = [{
- title: 'Manager',
- value: 'manager',
- }, {
- title: 'Observer',
- value: 'observer',
- }, {
- title: 'Account Manager',
- value: 'account_manager',
- }, {
- title: 'Account Executive',
- value: 'account_executive',
- }, {
- title: 'Program Manager',
- value: 'program_manager',
- }, {
- title: 'Solution Architect',
- value: 'solution_architect',
- }, {
- title: 'Project Manager',
- value: 'project_manager',
- }]
-
- this.handleRoles = this.handleRoles.bind(this)
- this.onConfirm = this.onConfirm.bind(this)
- }
-
- handleRoles(option) {
- this.setState({
- role: option.value
- })
- }
-
- onConfirm() {
- if(this.props.showRoleSelector) {
- this.props.onConfirm(this.state.role)
- } else {
- this.props.onConfirm()
- }
-
- }
-
- render()
- {
- const {onCancel, title, content, buttonColor, buttonText, isLoading, showRoleSelector, loadingTitle} = this.props
-
- return (
-
-
-
{isLoading ? loadingTitle : title}
- {isLoading ? (
-
- ) : (
-
-
- {showRoleSelector &&
-
- }
-
- )}
-
-
- Cancel
-
-
- {buttonText}
-
-
+const Dialog = ({onCancel, title, content, buttonColor, buttonText, isLoading, loadingTitle, onConfirm}) => (
+
+
+
{isLoading ? loadingTitle : title}
+ {isLoading ? (
+
+ ) : (
+
-
- )
- }
-}
+ )}
+
+
+ Cancel
+
+
+ {buttonText}
+
+
+
+
+)
Dialog.defaultProps = {
- showRoleSelector: false,
isLoading: false,
}
@@ -119,7 +52,6 @@ Dialog.propTypes = {
content: PT.string,
buttonColor: PT.string,
buttonText: PT.string,
- showRoleSelector: PT.bool,
isLoading: PT.bool,
}
diff --git a/src/components/TeamManagement/ProjectManagementDialog.js b/src/components/TeamManagement/ProjectManagementDialog.js
index 6cc49abea..537c1038f 100644
--- a/src/components/TeamManagement/ProjectManagementDialog.js
+++ b/src/components/TeamManagement/ProjectManagementDialog.js
@@ -40,12 +40,13 @@ class ProjectManagementDialog extends React.Component {
}
onChange(selectedMembers) {
- const { projectTeamInvites, members, topcoderTeamInvites } = this.props
+ const { projectTeamInvites, members, topcoderTeamInvites, copilotTeamInvites } = this.props
const present = _.some(selectedMembers, (selectedMember) => (
this.isSelectedMemberAlreadyInvited(members, selectedMember)
|| this.isSelectedMemberAlreadyInvited(topcoderTeamInvites, selectedMember)
|| this.isSelectedMemberAlreadyInvited(projectTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(copilotTeamInvites, selectedMember)
))
this.setState({
diff --git a/src/components/TeamManagement/TeamManagement.jsx b/src/components/TeamManagement/TeamManagement.jsx
index 31ca1b9e0..603f08683 100644
--- a/src/components/TeamManagement/TeamManagement.jsx
+++ b/src/components/TeamManagement/TeamManagement.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import uncontrollable from 'uncontrollable'
import './TeamManagement.scss'
import ProjectDialog from './ProjectManagementDialog'
+import CopilotDialog from './CopilotManagementDialog'
import TopcoderDialog from './TopcoderManagementDialog'
import MemberItem from './MemberItem'
import AddIcon from '../../assets/icons/icon-ui-bold-add.svg'
@@ -46,9 +47,12 @@ class TeamManagement extends React.Component {
this.state = {
topcoderTeamInviteButtonExpanded: false,
projectTeamInviteButtonExpanded: false,
+ copilotTeamInviteButtonExpanded: false,
}
this.projectTeamInviteButtonClick = this.projectTeamInviteButtonClick.bind(this)
this.topcoderTeamInviteButtonClick = this.topcoderTeamInviteButtonClick.bind(this)
+ this.copilotTeamInviteButtonClick = this.copilotTeamInviteButtonClick.bind(this)
+ this.onJoinConfirm = this.onJoinConfirm.bind(this)
}
topcoderTeamInviteButtonClick() {
@@ -61,6 +65,11 @@ class TeamManagement extends React.Component {
this.setState({projectTeamInviteButtonExpanded: !this.state.projectTeamInviteButtonExpanded})
}
+ copilotTeamInviteButtonClick() {
+ this.refreshStickyComp()
+ this.setState({copilotTeamInviteButtonExpanded: !this.state.copilotTeamInviteButtonExpanded})
+ }
+
refreshStickyComp() {
const event = document.createEvent('Event')
event.initEvent('refreshsticky', true, true)
@@ -77,33 +86,43 @@ class TeamManagement extends React.Component {
}
}
+ onJoinConfirm() {
+ const { onJoinConfirm } = this.props
+ // call without argument, so the role would be detected automatically
+ onJoinConfirm()
+ }
+
render() {
const {
currentUser, members, deletingMember, isAddingTeamMember, onMemberDeleteConfirm, onMemberDelete, isShowJoin,
- showNewMemberConfirmation, onJoin, onJoinConfirm, onShowProjectDialog, isShowProjectDialog,
+ showNewMemberConfirmation, onJoin, onShowProjectDialog, isShowProjectDialog,
projectTeamInvites, onProjectInviteDeleteConfirm, onProjectInviteSend, deletingInvite, changeRole,
onDeleteInvite, isShowTopcoderDialog, onShowTopcoderDialog, processingInvites, processingMembers,
onTopcoderInviteSend, onTopcoderInviteDeleteConfirm, topcoderTeamInvites, onAcceptOrRefuse, error,
- onSelectedMembersUpdate, selectedMembers, allMembers, updatingMemberIds
+ onSelectedMembersUpdate, selectedMembers, allMembers, updatingMemberIds, onShowCopilotDialog, copilotTeamInvites,
+ isShowCopilotDialog, onCopilotInviteSend,
} = this.props
-
const {
projectTeamInviteButtonExpanded,
topcoderTeamInviteButtonExpanded,
+ copilotTeamInviteButtonExpanded,
} = this.state
const currentMember = members.filter((member) => member.userId === currentUser.userId)[0]
const modalActive = isAddingTeamMember || deletingMember || isShowJoin || showNewMemberConfirmation || deletingInvite
const customerTeamManageAction = (currentUser.isAdmin || currentUser.isManager) && !currentMember
const topcoderTeamManageAction = hasPermission(PERMISSIONS.MANAGE_TOPCODER_TEAM)
+ const copilotTeamManageAction = hasPermission(PERMISSIONS.MANAGE_COPILOTS)
+ const canRequestCopilot = hasPermission(PERMISSIONS.REQUEST_COPILOTS)
const canJoinAsCopilot = !currentMember && currentUser.isCopilot
- const canJoinAsManager = !currentMember && (currentUser.isManager || currentUser.isAccountManager)
+ const canJoinTopcoderTeam = !currentMember && hasPermission(PERMISSIONS.JOIN_TOPCODER_TEAM)
const canShowInvite = currentMember && (currentMember.isCustomer || currentMember.isCopilot || currentMember.isManager)
const sortedMembers = members
let projectTeamInviteCount = 0
let topcoderTeamInviteCount = 0
+ let copilotTeamInviteCount = 0
return (
@@ -112,7 +131,7 @@ class TeamManagement extends React.Component {
Team
{(customerTeamManageAction) &&
onShowProjectDialog(true)}>
- Manage
+ {currentUser.isAdmin ? 'Manage' : 'View'}
}
@@ -173,6 +192,67 @@ class TeamManagement extends React.Component {
+
onJoin(true)}>
@@ -236,23 +313,18 @@ class TeamManagement extends React.Component {
{isShowJoin && ((() => {
const onClickCancel = () => onJoin(false)
- const onClickJoinConfirm = (role) => {
- onJoinConfirm(role)
- }
let role = 'Manager'
if (currentUser.isCopilot) role = 'Copilot'
- if (currentUser.isAccountManager) role = 'Account Manager'
return (
)
})())}
@@ -284,6 +356,35 @@ class TeamManagement extends React.Component {
/>
)
})())}
+ {(!modalActive && isShowCopilotDialog) && ((() => {
+ const onClickCancel = () => onShowCopilotDialog(false)
+ const removeMember = (member) => {
+ onMemberDelete(member)
+ }
+ const removeInvite = (item) => {
+ onDeleteInvite({item, type: 'copilot'})
+ }
+ return (
+
+ )
+ })())}
{(!modalActive && (isShowTopcoderDialog || this.props.history.location.hash === '#manageTopcoderTeam')) && ((() => {
const onClickCancel = () => {
this.props.history.push(this.props.history.location.pathname)
@@ -306,7 +407,7 @@ class TeamManagement extends React.Component {
isMember={!!currentMember}
onCancel={onClickCancel}
removeMember={removeMember}
- addUsers={onTopcoderInviteSend}
+ sendInvite={onTopcoderInviteSend}
approveOrDecline={onAcceptOrRefuse}
projectTeamInvites={projectTeamInvites}
topcoderTeamInvites={topcoderTeamInvites}
@@ -538,6 +639,7 @@ export default uncontrollable(TeamManagement, {
deletingMember: 'onMemberDelete',
isShowJoin: 'onJoin',
isShowProjectDialog: 'onShowProjectDialog',
+ isShowCopilotDialog: 'onShowCopilotDialog',
isShowTopcoderDialog: 'onShowTopcoderDialog',
deletingInvite: 'onDeleteInvite',
isInvited: 'onInviteAcceptShow'
diff --git a/src/components/TeamManagement/TeamManagement.scss b/src/components/TeamManagement/TeamManagement.scss
index e1f48debb..59c31f0c3 100644
--- a/src/components/TeamManagement/TeamManagement.scss
+++ b/src/components/TeamManagement/TeamManagement.scss
@@ -414,19 +414,11 @@
height: 200px;
}
- .topcoder-dialog {
- width: 750px;
- }
-
@media screen and (max-width: $screen-md - 1px) {
.dialog-body {
margin: 0 !important;
}
- .topcoder-dialog {
- width: unset;
- }
-
.project-dialog {
width: unset;
}
@@ -504,7 +496,8 @@
}
}
- .join-btn {
+ .join-btn,
+ a.join-btn {
@include roboto;
cursor: pointer;
padding: $base-unit*2 $base-unit*3;
diff --git a/src/components/TeamManagement/TopcoderManagementDialog.js b/src/components/TeamManagement/TopcoderManagementDialog.js
index 03ae312a8..428e2612f 100644
--- a/src/components/TeamManagement/TopcoderManagementDialog.js
+++ b/src/components/TeamManagement/TopcoderManagementDialog.js
@@ -1,14 +1,11 @@
import _ from 'lodash'
import React from 'react'
import PT from 'prop-types'
-import cn from 'classnames'
import moment from 'moment'
import Modal from 'react-modal'
import XMarkIcon from '../../assets/icons/icon-x-mark.svg'
import Avatar from 'appirio-tech-react-components/components/Avatar/Avatar'
import { getAvatarResized, getFullNameWithFallback } from '../../helpers/tcHelpers'
-import SelectDropdown from '../SelectDropdown/SelectDropdown'
-import Tooltip from 'appirio-tech-react-components/components/Tooltip/Tooltip'
import AutocompleteInputContainer from './AutocompleteInputContainer'
import {
PROJECT_MEMBER_INVITE_STATUS_REQUESTED, PROJECT_MEMBER_INVITE_STATUS_PENDING,
@@ -23,71 +20,24 @@ class TopcoderManagementDialog extends React.Component {
constructor(props) {
super(props)
this.state = {
- userRole: 'manager',
managerType: {},
showAlreadyMemberError: false,
errorMessage: null,
processingInviteRequestIds: [], // ids of invites for which request is being processed
}
- this.onUserRoleChange = this.onUserRoleChange.bind(this)
- this.handleRoles = this.handleRoles.bind(this)
- this.addUsers = this.addUsers.bind(this)
this.onChange = this.onChange.bind(this)
this.showIndividualErrors = this.showIndividualErrors.bind(this)
-
- this.roles = [{
- title: 'Manager',
- value: 'manager',
- }, {
- title: 'Observer',
- value: 'observer',
- }, {
- title: 'Copilot',
- value: 'copilot',
- canAddDirectly: true,
- }, {
- title: 'Account Manager',
- value: 'account_manager',
- }, {
- title: 'Account Executive',
- value: 'account_executive',
- }, {
- title: 'Program Manager',
- value: 'program_manager',
- }, {
- title: 'Solution Architect',
- value: 'solution_architect',
- }, {
- title: 'Project Manager',
- value: 'project_manager',
- }]
- }
-
- onUserRoleChange(memberId, id, type) {
- const managerType = Object.assign({}, this.state.managerType)
- managerType[memberId] = type
- this.props.changeRole(id, {role: this.roles.find((role) => role.title === type).value})
- this.setState({managerType})
- }
-
- handleRoles(option) {
- this.setState({
- userRole: option.value
- })
- }
-
- addUsers() {
- this.props.addUsers(this.state.userRole )
}
onChange(selectedMembers) {
- const { projectTeamInvites, members, topcoderTeamInvites } = this.props
+ const { projectTeamInvites, members, topcoderTeamInvites, copilotTeamInvites } = this.props
const present = _.some(selectedMembers, (selectedMember) => (
this.isSelectedMemberAlreadyInvited(members, selectedMember)
|| this.isSelectedMemberAlreadyInvited(topcoderTeamInvites, selectedMember)
|| this.isSelectedMemberAlreadyInvited(projectTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(copilotTeamInvites, selectedMember)
))
this.setState({
@@ -145,7 +95,7 @@ class TopcoderManagementDialog extends React.Component {
render() {
const {
members, currentUser, isMember, removeMember, onCancel, removeInvite, approveOrDecline, topcoderTeamInvites = [],
- selectedMembers, processingInvites, updatingMemberIds
+ selectedMembers, processingInvites,
} = this.props
const { processingInviteRequestIds } = this.state
const showRemove = hasPermission(PERMISSIONS.MANAGE_TOPCODER_TEAM)
@@ -162,7 +112,7 @@ class TopcoderManagementDialog extends React.Component {
contentLabel=""
>
-
+
Topcoder team
@@ -170,7 +120,7 @@ class TopcoderManagementDialog extends React.Component {
{(members.map((member) => {
- if (member.isCustomer) {
+ if (member.isCustomer || member.isCopilot) {
return null
}
i++
@@ -178,8 +128,6 @@ class TopcoderManagementDialog extends React.Component {
removeMember(member)
}
const userFullName = getFullNameWithFallback(member)
- const role = _.get(_.find(this.roles, r => r.value === member.role), 'title')
- const isMemberProcessing = _.includes(updatingMemberIds, member.id)
return (
}
- {(() => {
- if (!isMember || (!currentUser.isAdmin && !currentUser.isManager)) {
- return (
-
- )
- }
- let types = ['Copilot', 'Manager', 'Account Manager', 'Account Executive', 'Program Manager', 'Solution Architect', 'Project Manager']
- const currentType = role
- types = currentType === 'Observer'? ['Observer', ...types] : [...types]
- const onClick = (type) => {
- this.onUserRoleChange(member.userId, member.id, type)
- }
- return (
-
- {
- isMemberProcessing ?
:
- types.map((type) => {
- const isCopilotDisabled =
- type === 'Copilot' &&
- type !== currentType &&
- !(currentUser.isCopilotManager || currentUser.isAdmin)
-
- return (
- isCopilotDisabled ? (
-
-
-
- {'Only Connect Copilot Managers can change member role to copilots.'}
-
-
- ) : (
-
onClick(type)}
- className={cn('member-role', { active: type === currentType })}
- >
- {type}
-
- )
- )
- })
- }
-
- )
- })()}
)
}))}
@@ -361,15 +256,6 @@ class TopcoderManagementDialog extends React.Component {
{ this.state.showAlreadyMemberError &&
Project Member(s) can\'t be invited again. Please remove them from list.
}
-
- role.title !== 'Observer')}
- onSelect={this.handleRoles}
- />
-
{ this.state.errorMessage &&
{this.state.errorMessage}
}
@@ -377,11 +263,9 @@ class TopcoderManagementDialog extends React.Component {
className="tc-btn tc-btn-primary tc-btn-md"
type="submit"
disabled={processingInvites || this.state.showAlreadyMemberError || selectedMembers.length === 0}
- onClick={this.addUsers}
+ onClick={this.props.sendInvite}
>
- {_.find(this.roles, {value:this.state.userRole}).canAddDirectly && !showApproveDecline
- ?'Request invite'
- :'Invite users'}
+ Invite users
}
@@ -409,7 +293,7 @@ TopcoderManagementDialog.propTypes = {
changeRole: PT.func.isRequired,
projectTeamInvites: PT.arrayOf(PT.object),
topcoderTeamInvites: PT.arrayOf(PT.object),
- addUsers: PT.func.isRequired,
+ sendInvite: PT.func.isRequired,
approveOrDecline: PT.func.isRequired,
removeInvite: PT.func.isRequired,
onSelectedMembersUpdate: PT.func.isRequired,
diff --git a/src/components/UserSidebar/UserSidebar.jsx b/src/components/UserSidebar/UserSidebar.jsx
index 484d38af2..900272212 100644
--- a/src/components/UserSidebar/UserSidebar.jsx
+++ b/src/components/UserSidebar/UserSidebar.jsx
@@ -15,6 +15,7 @@ import NotificationSettingsIcon from '../../assets/icons/v.2.5/icon-notification
import AccountSecurityIcon from '../../assets/icons/v.2.5/icon-account-security.svg'
import './UserSidebar.scss'
+import FooterNeedHelp from '../FooterNeedHelp/FooterNeedHelp'
const navLinks = [{
label: 'ALL PROJECTS',
@@ -62,6 +63,11 @@ const navLinks = [{
to: '/faqs',
Icon: FAQIcon,
iconClassName: 'fill',
+}, {
+ label: 'GIVE APPLICATION FEEDBACK',
+ to: '/',
+ Icon: FAQIcon,
+ iconClassName: 'fill',
}]
class UserSidebar extends React.Component {
@@ -117,6 +123,7 @@ class UserSidebar extends React.Component {
SYSTEM
this.setAccordionOpen(i, open)} />
+
)
diff --git a/src/config/constants.js b/src/config/constants.js
index b7bdf17b8..cd068c2d0 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -688,6 +688,24 @@ export const MANAGER_ROLES = [
ROLE_PROJECT_MANAGER,
]
+/**
+ * Is user has any of these roles, it means such a user is not a customer.
+ */
+export const NON_CUSTOMER_ROLES = [
+ ROLE_CONNECT_COPILOT,
+ ROLE_CONNECT_MANAGER,
+ ROLE_CONNECT_ACCOUNT_MANAGER,
+ ROLE_CONNECT_ADMIN,
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_COPILOT_MANAGER,
+ ROLE_BUSINESS_DEVELOPMENT_REPRESENTATIVE,
+ ROLE_PRESALES,
+ ROLE_ACCOUNT_EXECUTIVE,
+ ROLE_PROGRAM_MANAGER,
+ ROLE_SOLUTION_ARCHITECT,
+ ROLE_PROJECT_MANAGER,
+]
+
// to be able to start the Connect App we should pass at least the dummy value for `FILE_PICKER_API_KEY`
// but if we want to test file uploading we should provide the real value in `FILE_PICKER_API_KEY` env variable
export const FILE_PICKER_API_KEY = process.env.FILE_PICKER_API_KEY || 'DUMMY'
@@ -1011,4 +1029,63 @@ export const INTERNAL_PROJECT_URLS=[
/**
* Project category string
*/
-export const PROJECT_CATEGORY_TAAS = 'talent-as-a-service'
\ No newline at end of file
+export const PROJECT_CATEGORY_TAAS = 'talent-as-a-service'
+
+/**
+ * Config for User Profile fields
+ *
+ * - `true` means field is required
+ * - `false` means field is optional
+ * - if field is not on the list means it should not be shown
+ */
+export const PROFILE_FIELDS_CONFIG = {
+ TOPCODER: {
+ // required fields
+ firstName: true,
+ lastName: true,
+ title: true,
+ country: true,
+ timeZone: true,
+ workingHourStart: true,
+ workingHourEnd: true,
+
+ // optional fields
+ avatar: false,
+ // companyName: false,
+ // companyURL: false,
+ // businessPhone: false,
+ },
+ CUSTOMER: {
+ // required fields
+ firstName: true,
+ lastName: true,
+ country: true,
+ title: true,
+ companyName: true,
+ companyURL: true,
+ businessPhone: true,
+ businessEmail: true,
+
+ // optional fields
+ avatar: false,
+ timeZone: false,
+ workingHourStart: false,
+ workingHourEnd: false,
+ },
+ COPILOT: {
+ // required fields
+ firstName: true,
+ lastName: true,
+ country: true,
+ timeZone: true,
+ workingHourStart: true,
+ workingHourEnd: true,
+
+ // optional fields
+ avatar: false,
+ // title: false,
+ // companyName: false,
+ // companyURL: true,
+ // businessPhone: false,
+ }
+}
diff --git a/src/config/permissions.js b/src/config/permissions.js
index cf5c0ff54..c5857f609 100644
--- a/src/config/permissions.js
+++ b/src/config/permissions.js
@@ -196,6 +196,45 @@ export default {
]
},
+ JOIN_TOPCODER_TEAM: {
+ _meta: {
+ group: 'Project Members',
+ title: 'Join topcoder team',
+ description: 'Join Topcoder Team without invitation',
+ },
+ topcoderRoles: [
+ ...TOPCODER_ADMINS,
+ ROLE_CONNECT_MANAGER,
+ ]
+ },
+
+ MANAGE_COPILOTS: {
+ _meta: {
+ group: 'Project Members',
+ title: 'Manage copilots',
+ description: 'Directly invite copilots to the project.',
+ },
+ topcoderRoles: [
+ ...TOPCODER_ADMINS,
+ ROLE_CONNECT_COPILOT_MANAGER
+ ]
+ },
+
+ REQUEST_COPILOTS: {
+ _meta: {
+ group: 'Project Members',
+ title: 'Request copilots',
+ description: 'Request copilots to the project.',
+ },
+ projectRoles: [
+ ..._.difference(PROJECT_ALL, [PROJECT_ROLE_COPILOT, PROJECT_ROLE_CUSTOMER])
+ ],
+ topcoderRoles: [
+ ...TOPCODER_ADMINS,
+ ROLE_CONNECT_COPILOT_MANAGER
+ ]
+ },
+
ACCESS_PRIVATE_POST: {
_meta: {
group: 'Topics & Posts',
@@ -292,6 +331,43 @@ export default {
],
},
+ VIEW_USER_PROFILE_AS_COPILOT: {
+ _meta: {
+ group: 'User Profile',
+ title: 'View User Profile as Copilot',
+ },
+ topcoderRoles: [
+ ROLE_CONNECT_COPILOT
+ ],
+ },
+
+ VIEW_USER_PROFILE_AS_TOPCODER_EMPLOYEE: {
+ _meta: {
+ group: 'User Profile',
+ title: 'View User Profile as Topcoder Employee',
+ },
+ topcoderRoles: [
+ ..._.difference(TOPCODER_ALL, [ROLE_TOPCODER_USER, ROLE_CONNECT_COPILOT])
+ ],
+ },
+
+ VIEW_USER_PROFILE_AS_CUSTOMER: {
+ _meta: {
+ group: 'User Profile',
+ title: 'View User Profile as Customer',
+ },
+ allowRule: {
+ topcoderRoles: [
+ ROLE_TOPCODER_USER
+ ]
+ },
+ denyRule: {
+ topcoderRoles: [
+ ..._.difference(TOPCODER_ALL, [ROLE_TOPCODER_USER])
+ ],
+ }
+ },
+
SEE_MEMBER_SUGGESTIONS: {
_meta: {
group: 'View Member Suggestions',
diff --git a/src/helpers/tcHelpers.js b/src/helpers/tcHelpers.js
index 37f1f4b98..d86af5825 100644
--- a/src/helpers/tcHelpers.js
+++ b/src/helpers/tcHelpers.js
@@ -5,8 +5,12 @@ import {
DISCOURSE_BOT_USERID,
CODER_BOT_USERID,
TC_SYSTEM_USERID,
- TC_CDN_URL
+ TC_CDN_URL,
+ NON_CUSTOMER_ROLES,
+ PROFILE_FIELDS_CONFIG,
} from '../config/constants'
+import { hasPermission } from './permissions'
+import PERMISSIONS from '../config/permissions'
/**
* Check if a user is a special system user
@@ -44,4 +48,49 @@ export const getFullNameWithFallback = (user) => {
userFullName = userFullName && userFullName.trim().length > 0 ? userFullName : user.handle
userFullName = userFullName && userFullName.trim().length > 0 ? userFullName : 'Connect user'
return userFullName
-}
\ No newline at end of file
+}
+
+/**
+ * Check if user profile is complete or no.
+ *
+ * @param {Object} user `loadUser.user` from Redux Store
+ * @param {Object} profileSettings profile settings with traits
+ *
+ * @returns {Boolean} complete or no
+ */
+export const isUserProfileComplete = (user, profileSettings) => {
+ const isTopcoderUser = _.intersection(user.roles, NON_CUSTOMER_ROLES).length > 0
+ const fieldsConfig = isTopcoderUser ? PROFILE_FIELDS_CONFIG.TOPCODER : PROFILE_FIELDS_CONFIG.CUSTOMER
+
+ // check if any required field doesn't have a value
+ let isMissingUserInfo = false
+ _.forEach(_.keys(fieldsConfig), (fieldKey) => {
+ const isFieldRequired = fieldsConfig[fieldKey]
+
+ if (isFieldRequired && !profileSettings[fieldKey]) {
+ isMissingUserInfo = true
+ return false
+ }
+ })
+
+ return !isMissingUserInfo
+}
+
+/**
+ * Get User Profile fields config based on the current user roles.
+ *
+ * @returns {Object} fields config
+ */
+export const getUserProfileFieldsConfig = () => {
+ if (hasPermission(PERMISSIONS.VIEW_USER_PROFILE_AS_TOPCODER_EMPLOYEE)) {
+ return PROFILE_FIELDS_CONFIG.TOPCODER
+ }
+
+ if (hasPermission(PERMISSIONS.VIEW_USER_PROFILE_AS_COPILOT)) {
+ return PROFILE_FIELDS_CONFIG.COPILOT
+ }
+
+ if (hasPermission(PERMISSIONS.VIEW_USER_PROFILE_AS_CUSTOMER)) {
+ return PROFILE_FIELDS_CONFIG.CUSTOMER
+ }
+}
diff --git a/src/projects/create/components/FillProjectDetails.js b/src/projects/create/components/FillProjectDetails.js
index 41a113073..b0f8f238c 100644
--- a/src/projects/create/components/FillProjectDetails.js
+++ b/src/projects/create/components/FillProjectDetails.js
@@ -16,7 +16,6 @@ import {
uploadProfilePhoto,
resetProfileSetting,
} from '../../../routes/settings/actions/index'
-import { getDefaultTopcoderRole } from '../../../helpers/permissions'
import {
ROLE_CONNECT_COPILOT,
ROLE_CONNECT_MANAGER,
@@ -24,13 +23,9 @@ import {
ROLE_CONNECT_COPILOT_MANAGER,
ROLE_ADMINISTRATOR,
ROLE_CONNECT_ADMIN,
- ROLE_BUSINESS_DEVELOPMENT_REPRESENTATIVE,
- ROLE_PRESALES,
- ROLE_ACCOUNT_EXECUTIVE,
- ROLE_PROGRAM_MANAGER,
- ROLE_SOLUTION_ARCHITECT,
- ROLE_PROJECT_MANAGER,
+ NON_CUSTOMER_ROLES,
} from '../../../config/constants'
+import { isUserProfileComplete } from '../../../helpers/tcHelpers'
class FillProjectDetails extends Component {
constructor(props) {
@@ -288,50 +283,8 @@ const mapStateToProps = ({ settings, loadUser }) => {
ROLE_CONNECT_ADMIN,
ROLE_CONNECT_MANAGER,
]
- const topCoderRoles = [
- ROLE_CONNECT_COPILOT,
- ROLE_CONNECT_MANAGER,
- ROLE_CONNECT_ACCOUNT_MANAGER,
- ROLE_CONNECT_ADMIN,
- ROLE_ADMINISTRATOR,
- ROLE_CONNECT_COPILOT_MANAGER,
- ROLE_BUSINESS_DEVELOPMENT_REPRESENTATIVE,
- ROLE_PRESALES,
- ROLE_ACCOUNT_EXECUTIVE,
- ROLE_PROGRAM_MANAGER,
- ROLE_SOLUTION_ARCHITECT,
- ROLE_PROJECT_MANAGER,
- ]
- const isTopcoderUser = _.intersection(loadUser.user.roles, topCoderRoles).length > 0
- let isMissingUserInfo = true
+ const isTopcoderUser = _.intersection(loadUser.user.roles, NON_CUSTOMER_ROLES).length > 0
const profileSettings = formatProfileSettings(settings.profile.traits)
- if (isTopcoderUser) {
- // We don't ask Topcoder User for "Company Name" and "Title"
- // but server requires them, so if they are not yet defined, we set them automatically
- if (!profileSettings.companyName) {
- profileSettings.companyName = 'Topcoder'
- }
- if (!profileSettings.title) {
- profileSettings.title = getDefaultTopcoderRole(loadUser.user)
- }
- isMissingUserInfo =
- !profileSettings.firstName ||
- !profileSettings.lastName ||
- !profileSettings.country ||
- !profileSettings.timeZone ||
- !profileSettings.workingHourStart ||
- !profileSettings.workingHourEnd
- } else {
- if (!profileSettings.businessEmail) {
- profileSettings.businessEmail = loadUser.user.email
- }
- isMissingUserInfo =
- !profileSettings.firstName ||
- !profileSettings.lastName ||
- !profileSettings.title ||
- !profileSettings.companyName ||
- !profileSettings.businessPhone
- }
return {
profileSettings: {
@@ -348,7 +301,7 @@ const mapStateToProps = ({ settings, loadUser }) => {
(role) => role === ROLE_CONNECT_COPILOT
),
isTopcoderUser,
- isMissingUserInfo,
+ isMissingUserInfo: !isUserProfileComplete(loadUser.user, profileSettings),
}
}
diff --git a/src/projects/create/components/UpdateUserInfo.js b/src/projects/create/components/UpdateUserInfo.js
index ae8fbfad5..96e5fc010 100644
--- a/src/projects/create/components/UpdateUserInfo.js
+++ b/src/projects/create/components/UpdateUserInfo.js
@@ -1,13 +1,10 @@
import React, { Component } from 'react'
import PT from 'prop-types'
-
import ModalControl from '../../../components/ModalControl'
import TailLeft from '../../../assets/icons/arrows-16px-1_tail-left.svg'
import { DOMAIN } from '../../../config/constants'
-
-import ProfileSettingsForm from '../../../routes/settings/routes/profile/components/ProfileSettingsForm'
-
import './UpdateUserInfo.scss'
+import IncompleteUserProfile from '../../../components/IncompleteUserProfile/IncompleteUserProfile'
class UpdateUserInfo extends Component {
constructor(props) {
@@ -25,12 +22,9 @@ class UpdateUserInfo extends Component {
const {
profileSettings,
saveProfileSettings,
- uploadProfilePhoto,
- isCustomer,
- isCopilot,
- isManager,
isTopcoderUser,
closeUserSettings,
+ user,
} = this.props
return (
@@ -51,27 +45,14 @@ class UpdateUserInfo extends Component {
Please complete your profile information below to able to submit
your project request.
-
{!isTopcoderUser && (
diff --git a/src/projects/detail/ProjectDetail.jsx b/src/projects/detail/ProjectDetail.jsx
index 87b59ed93..98287d892 100644
--- a/src/projects/detail/ProjectDetail.jsx
+++ b/src/projects/detail/ProjectDetail.jsx
@@ -15,13 +15,17 @@ import { getEmptyProjectObject } from '../reducers/project'
import {
LOAD_PROJECT_FAILURE, PROJECT_ROLE_CUSTOMER, PROJECT_ROLE_OWNER,
ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN, ROLE_CONNECT_COPILOT, ROLE_CONNECT_MANAGER,
- PROJECT_MEMBER_INVITE_STATUS_ACCEPTED, PROJECT_MEMBER_INVITE_STATUS_REFUSED, ACCEPT_OR_REFUSE_INVITE_FAILURE
+ PROJECT_MEMBER_INVITE_STATUS_ACCEPTED, PROJECT_MEMBER_INVITE_STATUS_REFUSED, ACCEPT_OR_REFUSE_INVITE_FAILURE, NON_CUSTOMER_ROLES
} from '../../config/constants'
import spinnerWhileLoading from '../../components/LoadingSpinner'
import CoderBot from '../../components/CoderBot/CoderBot'
import { getProjectProductTemplates, getProjectTemplateById } from '../../helpers/templates'
import Dialog from '../../components/TeamManagement/Dialog'
import { getProductEstimate } from '../../config/projectWizard'
+import { getProfileSettings, saveProfileSettings } from '../../routes/settings/actions'
+import { formatProfileSettings } from '../../routes/settings/helpers/settings'
+import { isUserProfileComplete } from '../../helpers/tcHelpers'
+import IncompleteUserProfileDialog from '../../components/IncomepleteUserProfileDialog/IncompleteUserProfileDialog'
const JOIN_INVITE_TITLE = 'You\'re invited to join this project'
@@ -60,7 +64,9 @@ const errorHandler = showCoderBotIfError(props => props.error && (props.error.ty
// This handles showing a spinner while the state is being loaded async
const spinner = spinnerWhileLoading(props =>
- !props.isLoading && (
+ !props.isLoading &&
+ // also wait until user profile is loaded to make sure we are showing incomplete user profile popup first, before showing project details
+ !props.profileSettings.isLoading && (
// first check that there are no error, before checking project properties
props.error && props.error.type === LOAD_PROJECT_FAILURE || props.error.type === ACCEPT_OR_REFUSE_INVITE_FAILURE ||
// old project or has projectTemplate loaded
@@ -127,6 +133,7 @@ class ProjectDetail extends Component {
isCallingInviteAction: false,
isUserAcceptedInvitation: undefined,
shouldForceCallAcceptRefuseRequest: false,
+ showIncompleteProfilePopup: false,
}
this.onUserInviteAction = this.onUserInviteAction.bind(this)
@@ -135,6 +142,8 @@ class ProjectDetail extends Component {
componentWillMount() {
const projectId = this.props.match.params.projectId
this.props.loadProjectDashboard(projectId)
+ // to check if user profile is complete or no
+ this.props.getProfileSettings()
// set flag that we have to force invitation action from the beginning
// so we can show appropriate loading indicator as soon as possible
@@ -152,7 +161,7 @@ class ProjectDetail extends Component {
}
componentWillReceiveProps(nextProps) {
- const {isProcessing, isLoading, error, project, match, showUserInvited} = nextProps
+ const {isProcessing, isLoading, error, project, match, showUserInvited, profileSettings} = nextProps
// handle just deleted projects
if (! (error || isLoading || isProcessing) && _.isEqual(getEmptyProjectObject(), project))
this.props.history.push('/projects/')
@@ -190,6 +199,16 @@ class ProjectDetail extends Component {
})
}
}
+
+ // as soon as user profile settings are loaded, check if all required fields are completed or show the popup
+ if (this.props.profileSettings.isLoading && !profileSettings.isLoading && !isUserProfileComplete(nextProps.currentUser, profileSettings.settings)) {
+ this.setState({ showIncompleteProfilePopup: true })
+ }
+
+ // as soon as user profile is updated and complete, close the popup
+ if (this.props.profileSettings.pending && !profileSettings.pending && isUserProfileComplete(nextProps.currentUser, profileSettings.settings)) {
+ this.setState({ showIncompleteProfilePopup: false })
+ }
}
getProjectRoleForCurrentUser({currentUserId, project}) {
@@ -252,7 +271,7 @@ class ProjectDetail extends Component {
}
render() {
- const { inviteError } = this.props
+ const { inviteError, project, profileSettings, currentUser, saveProfileSettings, isTopcoderUser } = this.props
const { isCallingInviteAction, isUserAcceptedInvitation, shouldForceCallAcceptRefuseRequest } = this.state
const currentMemberRole = this.getProjectRoleForCurrentUser(this.props)
const adminRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
@@ -263,7 +282,7 @@ class ProjectDetail extends Component {
const showUserInvited = this.props.showUserInvited
return (
- ((showUserInvited || shouldForceCallAcceptRefuseRequest) && !inviteError || isCallingInviteAction) ?
+ ((showUserInvited || shouldForceCallAcceptRefuseRequest) && !inviteError || isCallingInviteAction) ? (
this.onUserInviteAction(false)}
onConfirm={() => this.onUserInviteAction(true)}
@@ -273,22 +292,38 @@ class ProjectDetail extends Component {
buttonText="Join project"
buttonColor="blue"
isLoading={isCallingInviteAction || shouldForceCallAcceptRefuseRequest}
- /> :
-
+ ) : (
+
+ {this.state.showIncompleteProfilePopup && (
+ { this.setState({ showIncompleteProfilePopup: false })}}
+ title={`Welcome to ${project.name}`}
+ />
+ )}
+
+
+ )
)
}
}
-const mapStateToProps = ({projectState, projectDashboard, loadUser, productsTimelines, templates}) => {
+const mapStateToProps = ({projectState, projectDashboard, loadUser, productsTimelines, templates, settings}) => {
const templateId = (projectState.project || {}).templateId
const { projectTemplates, productTemplates } = templates
+ const isTopcoderUser = _.intersection(loadUser.user.roles, NON_CUSTOMER_ROLES).length > 0
return {
+ currentUser: loadUser.user,
currentUserId: parseInt(loadUser.user.id),
currentUserEmail: loadUser.user.email,
isLoading: projectDashboard.isLoading,
@@ -311,11 +346,16 @@ const mapStateToProps = ({projectState, projectDashboard, loadUser, productsTime
allProductTemplates: templates.productTemplates,
currentUserRoles: loadUser.user.roles,
showUserInvited: projectState.showUserInvited,
- userInvitationId: projectState.userInvitationId
+ userInvitationId: projectState.userInvitationId,
+ profileSettings: {
+ ...settings.profile,
+ settings: formatProfileSettings(settings.profile.traits)
+ },
+ isTopcoderUser,
}
}
-const mapDispatchToProps = { loadProjectDashboard, clearLoadedProject, acceptOrRefuseInvite, loadProjects }
+const mapDispatchToProps = { loadProjectDashboard, clearLoadedProject, acceptOrRefuseInvite, loadProjects, getProfileSettings, saveProfileSettings }
ProjectDetail.propTypes = {
project: PropTypes.object,
diff --git a/src/projects/detail/containers/FeedContainer.js b/src/projects/detail/containers/FeedContainer.js
index 991ab6dd8..4ab05e360 100644
--- a/src/projects/detail/containers/FeedContainer.js
+++ b/src/projects/detail/containers/FeedContainer.js
@@ -113,7 +113,7 @@ class FeedView extends React.Component {
return hasThread || hasComment
}
- mapFeed(feed, showAll = false, resetNewComment = false, prevProps) {
+ mapFeed(feed, showAll = false, resetNewComment = false, prevProps, currentProps) {
const { allMembers, project, currentMemberRole } = this.props
const item = _.pick(feed, ['id', 'date', 'read', 'tag', 'title', 'totalPosts', 'userId', 'reference', 'referenceId', 'postIds', 'isSavingTopic', 'isDeletingTopic', 'isAddingComment', 'isLoadingComments', 'error'])
// Github issue##623, allow comments on all posts (including system posts)
@@ -189,7 +189,8 @@ class FeedView extends React.Component {
item.newComment = ''
if (!resetNewComment) {
const feedFromState = _.find(this.state.feeds, f => feed.id === f.id)
- item.newComment = feedFromState ? feedFromState.newComment : ''
+ const preservedNewComment =_.find(currentProps && currentProps.preservedNewComments, { feedId: feed.id })
+ item.newComment = (feedFromState || preservedNewComment) ? (feedFromState || preservedNewComment).newComment : ''
}
item.hasMoreComments = item.comments.length !== item.totalComments
// adds permalink for the feed
@@ -214,7 +215,7 @@ class FeedView extends React.Component {
}
// reset new comment if we were adding comment and there is no error in doing so
const resetNewComment = prevFeed && prevFeed.isAddingComment && !feed.isAddingComment && !feed.error
- return this.mapFeed(feed, this.state.showAll.indexOf(feed.id) > -1, resetNewComment, prevProps)
+ return this.mapFeed(feed, this.state.showAll.indexOf(feed.id) > -1, resetNewComment, prevProps, props)
}).filter(item => item)
})
}
@@ -247,6 +248,8 @@ class FeedView extends React.Component {
return item
})
})
+ // also save new comment to a place where we can keep it even during feed reloading
+ this.props.updatePreservedNewComment(feedId, content)
}
onShowAllComments(feedId) {
@@ -489,6 +492,32 @@ const EnhancedFeedView = enhance(FeedView)
class FeedContainer extends React.Component {
constructor(props) {
super(props)
+
+ this.state = {
+ // as we loose `newComment` in the state inside `EnhancedFeedView` we have to keep it here
+ // so we can restore the currently edited new comment, in case the feed has been reloaded
+ preservedNewComments: []
+ }
+
+ this.updatePreservedNewComment = this.updatePreservedNewComment.bind(this)
+ }
+
+ updatePreservedNewComment (feedId, newComment) {
+ const feedIndex = _.findIndex(this.state.preservedNewComments, { feedId })
+
+ if (feedIndex !== -1) {
+ this.setState({
+ preservedNewComments: [
+ ...this.state.preservedNewComments.slice(0, feedIndex),
+ { feedId, newComment },
+ ...this.state.preservedNewComments.slice(feedIndex + 1),
+ ]
+ })
+ } else {
+ this.setState({
+ preservedNewComments: [...this.state.preservedNewComments, { feedId, newComment }]
+ })
+ }
}
componentWillMount() {
@@ -502,11 +531,14 @@ class FeedContainer extends React.Component {
}
render() {
+ console.log('preservedNewComments', this.state.preservedNewComments)
// Load only specified topics if topics input is available. Otherwise, load all feeds
const {feeds, topics} = this.props
const props = {
...this.props,
- feeds: topics ? feeds.filter(f => _.includes(topics, f.id)) : feeds
+ feeds: topics ? feeds.filter(f => _.includes(topics, f.id)) : feeds,
+ preservedNewComments: this.state.preservedNewComments,
+ updatePreservedNewComment: this.updatePreservedNewComment,
}
return
}
diff --git a/src/projects/detail/containers/ProjectInfoContainer.js b/src/projects/detail/containers/ProjectInfoContainer.js
index 8cdb794ed..3e5a2461d 100644
--- a/src/projects/detail/containers/ProjectInfoContainer.js
+++ b/src/projects/detail/containers/ProjectInfoContainer.js
@@ -41,6 +41,7 @@ import {
import { saveFeedComment } from '../../actions/projectTopics'
import TailLeft from '../../../assets/icons/arrows-16px-1_tail-left.svg'
+import FAQIcon from '../../../assets/icons/faq.svg'
import './ProjectInfoContainer.scss'
import MenuList from '../../../components/MenuList/MenuList'
@@ -51,6 +52,7 @@ import {
filterTopicAndPostChangedNotifications,
filterFileAndLinkChangedNotifications,
} from '../../../routes/notifications/helpers/notifications'
+import FooterNeedHelp from '../../../components/FooterNeedHelp/FooterNeedHelp'
const EnhancedProjectStatus = editableProjectStatus(ProjectStatus)
@@ -427,13 +429,13 @@ class ProjectInfoContainer extends React.Component {
const { showDeleteConfirm } = this.state
const { project, currentMemberRole, isSuperUser, phases, hideInfo, hideMembers,
productsTimelines, isProjectProcessing, notifications, projectTemplates } = this.props
-
+
const projectTemplateId = project.templateId
const projectTemplateKey = _.get(project, 'details.products[0]')
const projectTemplate = projectTemplateId
? _.find(projectTemplates, pt => pt.id === projectTemplateId)
: getProjectTemplateByKey(projectTemplates, projectTemplateKey)
-
+
const isTaaS = PROJECT_CATEGORY_TAAS === projectTemplate.category
let directLinks = null
// check if direct links need to be added
@@ -587,6 +589,21 @@ class ProjectInfoContainer extends React.Component {
{!hideMembers &&
}
+ {/* Separator above menulist */}
+
+
+
+
+
)
diff --git a/src/projects/detail/containers/TeamManagementContainer.jsx b/src/projects/detail/containers/TeamManagementContainer.jsx
index 88b2805bb..c77dc78f2 100644
--- a/src/projects/detail/containers/TeamManagementContainer.jsx
+++ b/src/projects/detail/containers/TeamManagementContainer.jsx
@@ -47,6 +47,7 @@ class TeamManagementContainer extends Component {
}
this.onProjectInviteSend = this.onProjectInviteSend.bind(this)
+ this.onCopilotInviteSend = this.onCopilotInviteSend.bind(this)
this.onProjectInviteDelete = this.onProjectInviteDelete.bind(this)
this.onTopcoderInviteDelete = this.onTopcoderInviteDelete.bind(this)
this.onTopcoderInviteSend = this.onTopcoderInviteSend.bind(this)
@@ -90,7 +91,6 @@ class TeamManagementContainer extends Component {
const { currentUser, projectId, addProjectMember } = this.props
let defaultRole = PROJECT_ROLE_MANAGER
if (currentUser.isCopilot) defaultRole = PROJECT_ROLE_COPILOT
- if (currentUser.isAccountManager) defaultRole = PROJECT_ROLE_ACCOUNT_MANAGER
role = role || defaultRole
addProjectMember(
projectId,
@@ -102,9 +102,9 @@ class TeamManagementContainer extends Component {
this.props.deleteTopcoderMemberInvite(this.props.projectId, invite)
}
- onTopcoderInviteSend(role) {
+ onTopcoderInviteSend() {
const {handles, emails } = this.getEmailsAndHandles()
- this.props.inviteTopcoderMembers(this.props.projectId, {role, handles, emails})
+ this.props.inviteTopcoderMembers(this.props.projectId, {role: 'manager', handles, emails})
}
onProjectInviteDelete(invite) {
@@ -116,6 +116,11 @@ class TeamManagementContainer extends Component {
this.props.inviteProjectMembers(this.props.projectId, emails, handles)
}
+ onCopilotInviteSend() {
+ const {handles, emails} = this.getEmailsAndHandles()
+ this.props.inviteTopcoderMembers(this.props.projectId, { role: 'copilot', handles, emails })
+ }
+
onAcceptOrRefuse(invite) {
return this.props.acceptOrRefuseInvite(this.props.projectId, invite)
}
@@ -182,7 +187,7 @@ class TeamManagementContainer extends Component {
render() {
const projectMembers = this.anontateMemberProps()
- const {projectTeamInvites, topcoderTeamInvites } = this.props
+ const {projectTeamInvites, topcoderTeamInvites, copilotTeamInvites } = this.props
return (