Skip to content

Commit 86eb0c5

Browse files
authored
Merge pull request #4127 from appirio-tech/progressive-profile-improvements
Progressive profile improvements
2 parents 4780b18 + 4c48e51 commit 86eb0c5

File tree

9 files changed

+494
-220
lines changed

9 files changed

+494
-220
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Dialog which shows incomplete user profile.
3+
*/
4+
import React from 'react'
5+
import PT from 'prop-types'
6+
import Modal from 'react-modal'
7+
import IncompleteUserProfile from '../IncompleteUserProfile/IncompleteUserProfile'
8+
import XMarkIcon from '../../assets/icons/icon-x-mark.svg'
9+
import styles from './IncompleteUserProfileDialog.scss'
10+
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator'
11+
12+
const IncompleteUserProfileDialog = ({
13+
onCloseDialog,
14+
title,
15+
...restProps,
16+
}) => {
17+
return (
18+
<Modal
19+
isOpen
20+
className="project-dialog-conatiner"
21+
overlayClassName="management-dialog-overlay incomplete-profile-dialog-overlay"
22+
onRequestClose={onCloseDialog}
23+
contentLabel=""
24+
>
25+
<div className={`project-dialog ${styles.dialog}`}>
26+
<div className="dialog-title">
27+
<h3>{title}</h3>
28+
<p styleName="subtitle">Complete your profile now.</p>
29+
<span onClick={onCloseDialog}><XMarkIcon /></span>
30+
</div>
31+
32+
<div className={`dialog-body ${styles.body}`}>
33+
{restProps.profileSettings.pending && <div styleName="loadingOverlay"><LoadingIndicator /></div>}
34+
<IncompleteUserProfile
35+
{...restProps}
36+
submitButton="Save"
37+
buttonExtraClassName="tc-btn-md"
38+
/>
39+
</div>
40+
</div>
41+
</Modal>
42+
)
43+
}
44+
45+
IncompleteUserProfileDialog.propTypes = {
46+
profileSettings: PT.object.isRequired,
47+
saveProfileSettings: PT.func.isRequired,
48+
isTopcoderUser: PT.bool.isRequired,
49+
user: PT.object.isRequired,
50+
onCloseDialog: PT.func.isRequired,
51+
title: PT.string.isRequired,
52+
}
53+
54+
export default IncompleteUserProfileDialog
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
@import '~tc-ui/src/styles/tc-includes';
2+
@import '../../styles/includes';
3+
4+
:global(.management-dialog-overlay .project-dialog-conatiner .project-dialog) {
5+
&.dialog {
6+
width: 800px;
7+
}
8+
9+
:global(.dialog-body).body {
10+
max-height: 500px;
11+
padding-top: $base-unit * 4;
12+
position: relative;
13+
}
14+
}
15+
16+
:global(.incomplete-profile-dialog-overlay.management-dialog-overlay .project-dialog-conatiner .project-dialog .input-container) {
17+
display: block;
18+
background: transparent;
19+
border-top: 0;
20+
border-radius: 0;
21+
margin: 0;
22+
padding: 0;
23+
24+
input {
25+
margin: 0;
26+
}
27+
28+
:global(.dropdown-wrap) {
29+
margin: 0;
30+
width: 100px;
31+
}
32+
}
33+
34+
.subtitle {
35+
padding-top: $base-unit * 4;
36+
text-align: center;
37+
}
38+
39+
.loadingOverlay {
40+
align-items: center;
41+
background-color: #fff;
42+
display: flex;
43+
left: 0;
44+
height: 100%;
45+
justify-content: center;
46+
position: absolute;
47+
top: 0;
48+
width: 100%;
49+
z-index: 1;
50+
}
51+
52+
@media screen and (max-width: $screen-md - 1px) {
53+
:global(.management-dialog-overlay .project-dialog-conatiner .project-dialog) {
54+
&.dialog {
55+
width: 100%;
56+
}
57+
}
58+
}
59+
60+
@media screen and (max-height: 700px) {
61+
:global(.management-dialog-overlay .project-dialog-conatiner .project-dialog) {
62+
:global(.dialog-body).body {
63+
max-height: calc(100vh - 200px);
64+
}
65+
}
66+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Incomplete User Profile Form.
3+
*/
4+
import React from 'react'
5+
import PT from 'prop-types'
6+
import { PROFILE_FIELDS_CONFIG } from '../../config/constants'
7+
import ProfileSettingsForm from '../../routes/settings/routes/profile/components/ProfileSettingsForm'
8+
import { getDefaultTopcoderRole } from '../../helpers/permissions'
9+
10+
const IncompleteUserProfile = ({
11+
profileSettings,
12+
saveProfileSettings,
13+
isTopcoderUser,
14+
user,
15+
...restProps
16+
}) => {
17+
const fieldsConfig = isTopcoderUser ? PROFILE_FIELDS_CONFIG.TOPCODER : PROFILE_FIELDS_CONFIG.CUSTOMER
18+
// never show avatar
19+
delete fieldsConfig.avatar
20+
// config the form to only show required fields which doesn't have the value yet
21+
const missingFieldsConfig = _.reduce(fieldsConfig, (acc, isFieldRequired, fieldKey) => {
22+
if (isFieldRequired && !_.get(profileSettings, `settings.${fieldKey}`)) {
23+
acc[fieldKey] = isFieldRequired
24+
}
25+
return acc
26+
}, {})
27+
28+
// prefill some fields of the profile
29+
const prefilledProfileSettings = _.cloneDeep(profileSettings)
30+
31+
// if time zone is required and doesn't have a value yet,
32+
// then detect timezone using browser feature and prefill the form
33+
if (fieldsConfig.timeZone && !profileSettings.settings.timeZone) {
34+
prefilledProfileSettings.settings.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
35+
}
36+
37+
if (isTopcoderUser) {
38+
// We don't ask Topcoder User for "Company Name" and "Title"
39+
// but server requires them, so if they are not yet defined, we set them automatically
40+
if (!profileSettings.settings.companyName) {
41+
prefilledProfileSettings.settings.companyName = 'Topcoder'
42+
}
43+
if (!profileSettings.settings.title) {
44+
prefilledProfileSettings.settings.title = getDefaultTopcoderRole(user)
45+
}
46+
} else {
47+
// at the moment we don't let users to update their business email, so in case it's not set, use registration email
48+
if (!profileSettings.settings.businessEmail) {
49+
prefilledProfileSettings.settings.businessEmail = user.email
50+
}
51+
}
52+
53+
return (
54+
<ProfileSettingsForm
55+
{...restProps}
56+
values={prefilledProfileSettings}
57+
saveSettings={saveProfileSettings}
58+
fieldsConfig={missingFieldsConfig}
59+
shouldDoValidateOnStart
60+
shouldShowTitle={false}
61+
/>
62+
)
63+
}
64+
65+
IncompleteUserProfile.propTypes = {
66+
profileSettings: PT.object.isRequired,
67+
saveProfileSettings: PT.func.isRequired,
68+
isTopcoderUser: PT.bool.isRequired,
69+
user: PT.object.isRequired,
70+
}
71+
72+
export default IncompleteUserProfile

src/config/constants.js

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,24 @@ export const MANAGER_ROLES = [
688688
ROLE_PROJECT_MANAGER,
689689
]
690690

691+
/**
692+
* Is user has any of these roles, it means such a user is not a customer.
693+
*/
694+
export const NON_CUSTOMER_ROLES = [
695+
ROLE_CONNECT_COPILOT,
696+
ROLE_CONNECT_MANAGER,
697+
ROLE_CONNECT_ACCOUNT_MANAGER,
698+
ROLE_CONNECT_ADMIN,
699+
ROLE_ADMINISTRATOR,
700+
ROLE_CONNECT_COPILOT_MANAGER,
701+
ROLE_BUSINESS_DEVELOPMENT_REPRESENTATIVE,
702+
ROLE_PRESALES,
703+
ROLE_ACCOUNT_EXECUTIVE,
704+
ROLE_PROGRAM_MANAGER,
705+
ROLE_SOLUTION_ARCHITECT,
706+
ROLE_PROJECT_MANAGER,
707+
]
708+
691709
// to be able to start the Connect App we should pass at least the dummy value for `FILE_PICKER_API_KEY`
692710
// but if we want to test file uploading we should provide the real value in `FILE_PICKER_API_KEY` env variable
693711
export const FILE_PICKER_API_KEY = process.env.FILE_PICKER_API_KEY || 'DUMMY'
@@ -1011,4 +1029,63 @@ export const INTERNAL_PROJECT_URLS=[
10111029
/**
10121030
* Project category string
10131031
*/
1014-
export const PROJECT_CATEGORY_TAAS = 'talent-as-a-service'
1032+
export const PROJECT_CATEGORY_TAAS = 'talent-as-a-service'
1033+
1034+
/**
1035+
* Config for User Profile fields
1036+
*
1037+
* - `true` means field is required
1038+
* - `false` means field is optional
1039+
* - if field is not on the list means it should not be shown
1040+
*/
1041+
export const PROFILE_FIELDS_CONFIG = {
1042+
// this config is used to show any user profile
1043+
DEFAULT: {
1044+
// required fields
1045+
firstName: true,
1046+
lastName: true,
1047+
title: true,
1048+
timeZone: true,
1049+
businessPhone: true,
1050+
companyName: true,
1051+
1052+
// optional fields
1053+
country: false,
1054+
avatar: false,
1055+
workingHourStart: false,
1056+
workingHourEnd: false,
1057+
},
1058+
1059+
// configs below are used when we ask users to fill missing fields (progressive registration)
1060+
TOPCODER: {
1061+
// required fields
1062+
firstName: true,
1063+
lastName: true,
1064+
country: true,
1065+
timeZone: true,
1066+
workingHourStart: true,
1067+
workingHourEnd: true,
1068+
1069+
// optional fields
1070+
avatar: false,
1071+
title: false,
1072+
companyName: false,
1073+
businessPhone: false,
1074+
},
1075+
CUSTOMER: {
1076+
// required fields
1077+
firstName: true,
1078+
lastName: true,
1079+
title: true,
1080+
companyName: true,
1081+
businessPhone: true,
1082+
1083+
// optional fields
1084+
businessEmail: false,
1085+
avatar: false,
1086+
country: false,
1087+
timeZone: false,
1088+
workingHourStart: false,
1089+
workingHourEnd: false,
1090+
}
1091+
}

src/helpers/tcHelpers.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
DISCOURSE_BOT_USERID,
66
CODER_BOT_USERID,
77
TC_SYSTEM_USERID,
8-
TC_CDN_URL
8+
TC_CDN_URL,
9+
NON_CUSTOMER_ROLES,
10+
PROFILE_FIELDS_CONFIG,
911
} from '../config/constants'
1012

1113
/**
@@ -44,4 +46,30 @@ export const getFullNameWithFallback = (user) => {
4446
userFullName = userFullName && userFullName.trim().length > 0 ? userFullName : user.handle
4547
userFullName = userFullName && userFullName.trim().length > 0 ? userFullName : 'Connect user'
4648
return userFullName
47-
}
49+
}
50+
51+
/**
52+
* Check if user profile is complete or no.
53+
*
54+
* @param {Object} user `loadUser.user` from Redux Store
55+
* @param {Object} profileSettings profile settings with traits
56+
*
57+
* @returns {Boolean} complete or no
58+
*/
59+
export const isUserProfileComplete = (user, profileSettings) => {
60+
const isTopcoderUser = _.intersection(user.roles, NON_CUSTOMER_ROLES).length > 0
61+
const fieldsConfig = isTopcoderUser ? PROFILE_FIELDS_CONFIG.TOPCODER : PROFILE_FIELDS_CONFIG.CUSTOMER
62+
63+
// check if any required field doesn't have a value
64+
let isMissingUserInfo = false
65+
_.forEach(_.keys(fieldsConfig), (fieldKey) => {
66+
const isFieldRequired = fieldsConfig[fieldKey]
67+
68+
if (isFieldRequired && !profileSettings.fieldKey) {
69+
isMissingUserInfo = true
70+
return false
71+
}
72+
})
73+
74+
return !isMissingUserInfo
75+
}

0 commit comments

Comments
 (0)