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 @@

Project Members

Invite new members or delete them. There are some additional restrictions for some roles.
- copilot manager account_manager account_executive @@ -93,6 +92,48 @@

Project Members

Connect Admin
+
+
+ Join topcoder team +
JOIN_TOPCODER_TEAM
+
Join Topcoder Team without invitation
+
+
+ administrator + Connect Admin + Connect Manager +
+
+
+
+ Manage copilots +
MANAGE_COPILOTS
+
Directly invite copilots to the project.
+
+
+ administrator + Connect Admin + Connect Copilot Manager +
+
+
+
+ Request copilots +
REQUEST_COPILOTS
+
Request copilots to the project.
+
+
+ manager + account_manager + account_executive + project_manager + program_manager + solution_architect + administrator + Connect Admin + Connect Copilot Manager +
+

Topics & Posts

@@ -188,6 +229,7 @@

User Profile

administrator Connect Admin Connect Manager + Connect Copilot Manager Connect Account Manager Business Development Representative Presales @@ -208,6 +250,7 @@

User Profile

administrator Connect Admin Connect Manager + Connect Copilot Manager Connect Account Manager Business Development Representative Presales @@ -217,6 +260,58 @@

User Profile

Project Manager
+
+
+ View User Profile as Copilot +
VIEW_USER_PROFILE_AS_COPILOT
+
+
+
+ Connect Copilot +
+
+
+
+ View User Profile as Topcoder Employee +
VIEW_USER_PROFILE_AS_TOPCODER_EMPLOYEE
+
+
+
+ administrator + Connect Admin + Connect Manager + Connect Copilot Manager + Connect Account Manager + Business Development Representative + Presales + Account Executive + Program Manager + Solution Architect + Project Manager +
+
+
+
+ View User Profile as Customer +
VIEW_USER_PROFILE_AS_CUSTOMER
+
+
+
+ Topcoder User + administrator + Connect Admin + Connect Manager + Connect Copilot Manager + Connect Account Manager + Business Development Representative + Presales + Account Executive + Program Manager + Solution Architect + Project Manager + Connect Copilot +
+

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} +
} + +
+
+ +
+ ) + } +} + +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 && - - } -
- )} -
- - -
+const Dialog = ({onCancel, title, content, buttonColor, buttonText, isLoading, loadingTitle, onConfirm}) => ( + +
+
{isLoading ? loadingTitle : title}
+ {isLoading ? ( + + ) : ( +
+
- - ) - } -} + )} +
+ + +
+
+ +) 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 {
+
+
+
+ Copilot + {copilotTeamManageAction && + onShowCopilotDialog(true)}> + Manage + + } +
+
+ {sortedMembers.map((member, i) => { + if (!member.isCopilot) { + return + } + + copilotTeamInviteCount++ + if (!copilotTeamInviteButtonExpanded && copilotTeamInviteCount > 3) { + return null + } + + return ( + + ) + })} + {copilotTeamInvites.map((invite, i) => { + copilotTeamInviteCount++ + if(!copilotTeamInviteButtonExpanded && copilotTeamInviteCount > 3) { + return null + } + + return ( + + ) + })} + {copilotTeamInviteCount > 3 && +
+
+ {!copilotTeamInviteButtonExpanded ? 'Show All': 'Show Less'} +
+
+ } + {canRequestCopilot && + + } +
+
+
@@ -183,7 +263,7 @@ class TeamManagement extends React.Component {
{sortedMembers.map((member, i) => { - if (member.isCustomer) { + if (member.isCustomer || member.isCopilot) { return } @@ -197,9 +277,6 @@ class TeamManagement extends React.Component { ) })} {topcoderTeamInvites.map((invite, i) => { - if (invite.isCustomer) { - return - } topcoderTeamInviteCount++ if(!topcoderTeamInviteButtonExpanded &&topcoderTeamInviteCount > 3) { return null @@ -222,7 +299,7 @@ class TeamManagement extends React.Component {
} - { (canJoinAsCopilot || canJoinAsManager) && + { (canJoinAsCopilot || canJoinTopcoderTeam) &&
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 ( -
-
- {role} -
-
- ) - } - 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 ? ( - -
-
- {type} -
-
-
- {'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 (
{ processingMembers: projectState.processingMembers, updatingMemberIds: projectState.updatingMemberIds, error: projectState.error, - topcoderTeamInvites: _.filter(projectState.project.invites, i => i.role !== 'customer'), + topcoderTeamInvites: _.filter(projectState.project.invites, i => i.role !== 'customer' && i.role !== 'copilot'), + copilotTeamInvites: _.filter(projectState.project.invites, i => i.role === 'copilot'), projectTeamInvites: _.filter(projectState.project.invites, i => i.role === 'customer') } } diff --git a/src/routes/reports/components/ReportsToolBar.jsx b/src/routes/reports/components/ReportsToolBar.jsx new file mode 100644 index 000000000..0bc2f4f85 --- /dev/null +++ b/src/routes/reports/components/ReportsToolBar.jsx @@ -0,0 +1,11 @@ +/** + * Reports pages tool bar + */ +import React from 'react' +import SectionTopBar from '../../../components/TopBar/SectionToolBar' + +const ReportsToolBar = () => ( + +) + +export default ReportsToolBar diff --git a/src/routes/reports/routes.jsx b/src/routes/reports/routes.jsx index aff3e436a..c4454c22f 100644 --- a/src/routes/reports/routes.jsx +++ b/src/routes/reports/routes.jsx @@ -5,11 +5,10 @@ import React from 'react' import { Route } from 'react-router-dom' import { renderApp } from '../../components/App/App' import TopBarContainer from '../../components/TopBar/TopBarContainer' -import ProjectsToolBar from '../../components/TopBar/ProjectsToolBar' +import ReportsToolBar from './components/ReportsToolBar' import { requiresAuthentication } from '../../components/AuthenticatedComponent' import UserReportsContainer from './containers/UserReportsContainer' const UserReportsContainerWithAuth = requiresAuthentication(UserReportsContainer) -export default [ - , )} />, - -] +export default ( + , )} /> +) diff --git a/src/routes/settings/routes/profile/components/ProfileSettingsForm.jsx b/src/routes/settings/routes/profile/components/ProfileSettingsForm.jsx index dfdc31764..919cc0d9a 100644 --- a/src/routes/settings/routes/profile/components/ProfileSettingsForm.jsx +++ b/src/routes/settings/routes/profile/components/ProfileSettingsForm.jsx @@ -73,7 +73,7 @@ class ProfileSettingsForm extends Component { if (country && country.code) { if (previousSelectedCountry !== country.name && country.name) { // when country code of business phone changes, the country selection should change automatically - this.refs.countrySelect.setValue(country.name) + this.refs.countrySelect && this.refs.countrySelect.setValue(country.name) this.setState({ countrySelected: country.name, }) @@ -136,6 +136,11 @@ class ProfileSettingsForm extends Component { validationErrors.isEmail = 'Please, enter correct email' } + if (name === 'companyURL') { + validations.isRelaxedUrl = true + validationErrors.isRelaxedUrl = 'Please, enter correct URL' + } + return (
@@ -167,6 +172,13 @@ class ProfileSettingsForm extends Component { ...data, } + // if we don't show the country field, but we show the phone field and we chose + // a phone in another country, we have to send to the server updated country + // as we always change the country to the same as phone number + if (_.isUndefined(this.props.fieldsConfig.country) && this.state.countrySelected) { + updatedData.country = this.state.countrySelected + } + updatedData.businessPhone = formatPhone(updatedData.businessPhone) this.props.saveSettings(updatedData) @@ -192,15 +204,7 @@ class ProfileSettingsForm extends Component { render() { const { - showBusinessEmail, - showAvatar, - showCompanyName, - showTitle, - showBusinessPhone, - isRequiredTimeZone, - isRequiredCountry, - isRequiredWorkingHours, - isRequiredBusinessEmail, + fieldsConfig, submitButton, showBackButton, onBack, @@ -221,7 +225,7 @@ class ProfileSettingsForm extends Component { onChange={this.onChange} > {shouldShowTitle && (
Personal information
)} - {showAvatar && ( + {!_.isUndefined(fieldsConfig.avatar) && (
Avatar
)} - {this.getField('First Name', 'firstName', true)} - {this.getField('Last Name', 'lastName', true)} - {showTitle && this.getField('Title', 'title', true)} - {showBusinessEmail && - this.getField('Business Email', 'businessEmail', isRequiredBusinessEmail, true)} - {showBusinessPhone && ( + {!_.isUndefined(fieldsConfig.firstName) && this.getField('First Name', 'firstName', fieldsConfig.firstName)} + {!_.isUndefined(fieldsConfig.lastName) && this.getField('Last Name', 'lastName', fieldsConfig.lastName)} + {!_.isUndefined(fieldsConfig.title) && this.getField('Title', 'title', fieldsConfig.title)} + {!_.isUndefined(fieldsConfig.businessPhone) && (
Business Phone  - * + {fieldsConfig.businessPhone && *}
)} - {showCompanyName && - this.getField( - 'Company Name', - 'companyName', - true, - disableCompanyInput - )} -
- {isRequiredCountry ? ( -
- Country  - * -
- ) : ( -
- Country -
- )} -
- - {this.state.countrySelectionDirty && ( -
- Note: Changing the country also updates the country code of - business phone. + {!_.isUndefined(fieldsConfig.businessEmail) && this.getField('Business Email', 'businessEmail', fieldsConfig.businessEmail, true)} + {!_.isUndefined(fieldsConfig.companyName) && this.getField('Company Name', 'companyName', fieldsConfig.companyName, disableCompanyInput)} + {!_.isUndefined(fieldsConfig.companyURL) && this.getField('Company URL', 'companyURL', fieldsConfig.companyURL)} + {!_.isUndefined(fieldsConfig.country) && ( +
+ {fieldsConfig.country ? ( +
+ Country  + * +
+ ) : ( +
+ Country
)} -
-
-
- {isRequiredTimeZone ? ( -
- Local Timezone  - * -
- ) : ( -
- Local Timezone -
- )} -
- ( - - filterFn(option.data, searchText) - } - value={this.props.values.settings.timeZone || ''} - name="timeZone" - options={timezoneOptions} - required={isRequiredTimeZone} - validationError="Please enter Local Timezone" - /> +
+ + {this.state.countrySelectionDirty && !_.isUndefined(fieldsConfig.businessPhone) && ( +
+ Note: Changing the country also updates the country code of + business phone. +
)} - /> +
-
-
- {isRequiredWorkingHours ? ( -
- Normal Working Hours  - * + )} + {!_.isUndefined(fieldsConfig.timeZone) && ( +
+ {fieldsConfig.timeZone ? ( +
+ Local Timezone  + * +
+ ) : ( +
+ Local Timezone +
+ )} +
+ ( + + filterFn(option.data, searchText) + } + value={this.props.values.settings.timeZone || ''} + name="timeZone" + options={timezoneOptions} + required={fieldsConfig.timeZone} + validationError="Please enter Local Timezone" + /> + )} + />
- ) : ( -
- Normal Working Hours +
+ )} + {(!_.isUndefined(fieldsConfig.workingHourStart) || !_.isUndefined(fieldsConfig.workingHourStart)) && ( +
+ {(fieldsConfig.workingHourStart || fieldsConfig.workingHourStart) ? ( +
+ Normal Working Hours  + * +
+ ) : ( +
+ Normal Working Hours +
+ )} +
+
- )} -
-
-
+ )}
{showBackButton && ( @@ -408,15 +412,6 @@ class ProfileSettingsForm extends Component { } ProfileSettingsForm.defaultProps = { - showBusinessEmail: false, - showAvatar: true, - showCompanyName: true, - showTitle: true, - showBusinessPhone: true, - isRequiredTimeZone: true, - isRequiredCountry: false, - isRequiredWorkingHours: false, - isRequiredBusinessEmail: true, showBackButton: false, submitButton: 'Save settings', onBack: () => {}, @@ -429,15 +424,20 @@ ProfileSettingsForm.propTypes = { values: PropTypes.object.isRequired, saveSettings: PropTypes.func.isRequired, uploadPhoto: PropTypes.func.isRequired, - showBusinessEmail: PropTypes.bool, - showAvatar: PropTypes.bool, - showCompanyName: PropTypes.bool, - showTitle: PropTypes.bool, - showBusinessPhone: PropTypes.bool, - isRequiredTimeZone: PropTypes.bool, - isRequiredCountry: PropTypes.bool, - isRequiredWorkingHours: PropTypes.bool, - isRequiredBusinessEmail: PropTypes.bool, + fieldsConfig: PropTypes.shape({ + avatar: PropTypes.bool, + firstName: PropTypes.bool, + lastName: PropTypes.bool, + title: PropTypes.bool, + companyName: PropTypes.bool, + companyURL: PropTypes.bool, + businessPhone: PropTypes.bool, + businessEmail: PropTypes.bool, + country: PropTypes.bool, + timeZone: PropTypes.bool, + workingHourStart: PropTypes.bool, + workingHourEnd: PropTypes.bool, + }).isRequired, showBackButton: PropTypes.bool, shouldShowTitle: PropTypes.bool, shouldDoValidateOnStart: PropTypes.bool, diff --git a/src/routes/settings/routes/profile/containers/ProfileSettingsContainer.jsx b/src/routes/settings/routes/profile/containers/ProfileSettingsContainer.jsx index 2e94f89cf..55dc1dd3c 100644 --- a/src/routes/settings/routes/profile/containers/ProfileSettingsContainer.jsx +++ b/src/routes/settings/routes/profile/containers/ProfileSettingsContainer.jsx @@ -10,6 +10,7 @@ import spinnerWhileLoading from '../../../../../components/LoadingSpinner' import { requiresAuthentication } from '../../../../../components/AuthenticatedComponent' import { getProfileSettings, saveProfileSettings, uploadProfilePhoto } from '../../../actions/index' import { formatProfileSettings } from '../../../helpers/settings' +import { getUserProfileFieldsConfig } from '../../../../../helpers/tcHelpers' const enhance = spinnerWhileLoading(props => !props.values.isLoading) const ProfileSettingsFormEnhanced = enhance(ProfileSettingsForm) @@ -20,6 +21,7 @@ class ProfileSettingsContainer extends Component { render() { const { profileSettings, saveProfileSettings, uploadProfilePhoto, user } = this.props + const fieldsConfig = getUserProfileFieldsConfig() return ( ) diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss index dcda97c0c..03a50161e 100644 --- a/src/styles/_fonts.scss +++ b/src/styles/_fonts.scss @@ -6,6 +6,7 @@ font-family: '#{$font-name}'; src: url('#{$font-url}#{$font-file}.eot'); src: url('#{$font-url}#{$font-file}.eot?#iefix') format('embedded-opentype'), + url('#{$font-url}#{$font-file}.woff2') format('woff2'), url('#{$font-url}#{$font-file}.woff') format('woff'), url('#{$font-url}#{$font-file}.ttf') format('truetype'), url('#{$font-url}#{$font-file}.svg##{$font-name}') format('svg');