Skip to content

Commit 442a687

Browse files
authored
Merge branch 'dev' into feature/registration_and_profile_page_improvements
2 parents a72685c + 9b6e9d2 commit 442a687

File tree

9 files changed

+170
-31
lines changed

9 files changed

+170
-31
lines changed

src/config/constants.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -643,13 +643,15 @@ export const ROLE_CONNECT_ADMIN = 'Connect Admin'
643643
export const ROLE_ADMINISTRATOR = 'administrator'
644644
export const ROLE_CONNECT_COPILOT_MANAGER = 'Connect Copilot Manager'
645645

646+
// to be able to start the Connect App we should pass at least the dummy value for `FILE_PICKER_API_KEY`
647+
// but if we want to test file uploading we should provide the real value in `FILE_PICKER_API_KEY` env variable
648+
export const FILE_PICKER_API_KEY = process.env.FILE_PICKER_API_KEY || 'DUMMY'
646649
// FIXME .. remove defaults
647-
export const FILE_PICKER_API_KEY = process.env.FILE_PICKER_API_KEY || ''
648650
export const FILE_PICKER_SUBMISSION_CONTAINER_NAME = process.env.FILE_PICKER_SUBMISSION_CONTAINER_NAME || 'submission-staging-dev'
649651
export const FILE_PICKER_CNAME = process.env.FILE_PICKER_CNAME || 'fs.topcoder.com'
650652
export const FILE_PICKER_FROM_SOURCES = process.env.FILE_PICKER_FROM_SOURCES || ['local_file_system', 'googledrive', 'dropbox']
651653
export const PROJECT_ATTACHMENTS_FOLDER = process.env.PROJECT_ATTACHMENTS_FOLDER || 'PROJECT_ATTACHMENTS'
652-
export const FILE_PICKER_ACCEPT = process.env.FILE_PICKER_ACCEPT || ['.bmp', '.gif', '.jpg', '.tex', '.xls', '.xlsx', '.doc', '.docx', '.zip', '.txt', '.pdf', '.png', '.ppt', '.pptx', '.rtf']
654+
export const FILE_PICKER_ACCEPT = process.env.FILE_PICKER_ACCEPT || ['.bmp', '.gif', '.jpg', '.tex', '.xls', '.xlsx', '.doc', '.docx', '.zip', '.txt', '.pdf', '.png', '.ppt', '.pptx', '.rtf', '.csv']
653655

654656
export const SEGMENT_KEY = process.env.CONNECT_SEGMENT_KEY
655657
/*

src/helpers/permissions.js

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,116 @@
11
import store from '../config/store'
22
import _ from 'lodash'
33

4-
export const checkPermission = (permission, project, entity) => {
5-
const {projectRoles, topcoderRoles, handler} = permission
6-
const currentUser = _.get(store.getState(), 'loadUser.user', {})
7-
const roles = currentUser.roles || []
8-
9-
if(project && projectRoles){
10-
const currentProjectMember = _.find(project.members, m => m.userId===currentUser.userId)
11-
if (currentProjectMember){
12-
const currentUserRole = currentProjectMember.role
13-
if (projectRoles.includes(currentUserRole)){
14-
return true
15-
}
16-
}
4+
/**
5+
* Check if user has permission.
6+
* (The main permission method which should be used).
7+
*
8+
* This method uses permission defined in `permission`
9+
* and checks that the logged-in user from Redux Store matches it.
10+
*
11+
* `permission` may be defined in two ways:
12+
* - **Full** way with defined `allowRule` and optional `denyRule`, example:
13+
* ```js
14+
* {
15+
* allowRule: {
16+
* projectRoles: [],
17+
* topcoderRoles: []
18+
* },
19+
* denyRule: {
20+
* projectRoles: [],
21+
* topcoderRoles: []
22+
* }
23+
* }
24+
* ```
25+
* If user matches `denyRule` then the access would be dined even if matches `allowRule`.
26+
* - **Simplified** way may be used if we only want to define `allowRule`.
27+
* We can skip the `allowRule` property and define `allowRule` directly inside `permission` object, example:
28+
* ```js
29+
* {
30+
* projectRoles: [],
31+
* topcoderRoles: []
32+
* }
33+
* ```
34+
* This **simplified** permission is equal to a **full** permission:
35+
* ```js
36+
* {
37+
* allowRule: {
38+
* projectRoles: [],
39+
* topcoderRoles: []
40+
* }
41+
* }
42+
* ```
43+
*
44+
* If we define any rule with `projectRoles` list, we also should provide `project`
45+
* with the list of project members in `project.members`.
46+
*
47+
* @param {Object} permissionRule permission rule
48+
* @param {Array<String>} permissionRule.projectRoles the list of project roles of the user
49+
* @param {Array<String>} permissionRule.topcoderRoles the list of Topcoder roles of the user
50+
* @param {Object} [project] project object - required to check `topcoderRoles`
51+
* @param {Array} project.members list of project members - required to check `topcoderRoles`
52+
*
53+
* @returns {Boolean} true, if has permission
54+
*/
55+
export const checkPermission = (permission, project) => {
56+
const user = _.get(store.getState(), 'loadUser.user', {})
57+
58+
const allowRule = permission.allowRule ? permission.allowRule : permission
59+
const denyRule = permission.denyRule ? permission.denyRule : null
60+
61+
const allow = matchPermissionRule(allowRule, user, project)
62+
const deny = matchPermissionRule(denyRule, user, project)
63+
64+
return allow && !deny
65+
}
66+
67+
/**
68+
* Check if user match the permission rule.
69+
* (Helper method, most likely wouldn't be used directly).
70+
*
71+
* This method uses permission rule defined in `permissionRule`
72+
* and checks that the `user` matches it.
73+
*
74+
* If we define a rule with `projectRoles` list, we also should provide `projectMembers`
75+
* - the list of project members.
76+
*
77+
* @param {Object} permissionRule permission rule
78+
* @param {Array<String>} permissionRule.projectRoles the list of project roles of the user
79+
* @param {Array<String>} permissionRule.topcoderRoles the list of Topcoder roles of the user
80+
* @param {Object} user user for whom we check permissions
81+
* @param {Object} user.roles list of user roles
82+
* @param {Object} [project] project object - required to check `topcoderRoles`
83+
* @param {Array} project.members list of project members - required to check `topcoderRoles`
84+
*
85+
* @returns {Boolean} true, if has permission
86+
*/
87+
const matchPermissionRule = (permissionRule, user, project) => {
88+
let hasProjectRole = false
89+
let hasTopcoderRole = false
90+
91+
// if no rule defined, no access by default
92+
if (!permissionRule) {
93+
return false
1794
}
1895

19-
if(topcoderRoles && roles.some(role => topcoderRoles.indexOf(role) !== -1)) {
20-
return true
96+
// check Project Roles
97+
if (permissionRule.projectRoles
98+
&& permissionRule.projectRoles.length > 0
99+
&& project
100+
&& project.members
101+
) {
102+
const userId = !_.isNumber(user.userId) ? parseInt(user.userId, 10) : user.userId
103+
const member = _.find(project.members, { userId })
104+
hasProjectRole = member && _.includes(permissionRule.projectRoles, member.role)
21105
}
22106

23-
if(handler && handler(currentUser, entity, project)) {
24-
return true
107+
// check Topcoder Roles
108+
if (permissionRule.topcoderRoles && permissionRule.topcoderRoles.length > 0) {
109+
hasTopcoderRole = _.intersection(
110+
_.get(user, 'roles', []).map(role => role.toLowerCase()),
111+
permissionRule.topcoderRoles.map(role => role.toLowerCase())
112+
).length > 0
25113
}
26114

27-
return false
28-
}
115+
return hasProjectRole || hasTopcoderRole
116+
}

src/helpers/wizardHelper.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import _ from 'lodash'
2323
import update from 'react-addons-update'
2424
import { evaluate, getFieldNamesFromExpression, populatePreparedConditions } from 'expression-evaluator'
2525
import { flatten, unflatten } from 'flat'
26+
import { checkPermission } from './permissions'
2627

2728
/**
2829
* @typedef {Object} NodeObject
@@ -392,6 +393,13 @@ export const initWizard = (template, project, productTemplates, incompleteWizard
392393
node
393394
}
394395

396+
// precalculate the result for `userPermissionCondition`
397+
if (nodeObject.userPermissionCondition && !checkPermission(nodeObject.userPermissionCondition)) {
398+
// we calculate this value once as user permissions cannot be changed
399+
// and we would use this value to calculate value of `hiddenByCondition`
400+
nodeObject.__wizard.hiddenByPermission = true
401+
}
402+
395403
// add all found variables from condition to the list of dependant fields of the template
396404
if (nodeObject.condition) {
397405
wizardTemplate.__wizard.dependantFields = _.uniq([
@@ -426,6 +434,11 @@ export const initWizard = (template, project, productTemplates, incompleteWizard
426434
prevWizardStep = getPrevStepToShow(wizardTemplate, lastWizardStep)
427435
}
428436

437+
// if the first step we found is hidden, then search for the next step which is not hidden
438+
if (!isNodeVisible(wizardTemplate, currentWizardStep)) {
439+
currentWizardStep = getStepToShowByDir(wizardTemplate, currentWizardStep, NODE_DIR.NEXT)
440+
}
441+
// use provided `lastWizardStep` or use the first non-hidden step
429442
currentWizardStep = lastWizardStep || currentWizardStep
430443
}
431444

@@ -880,7 +893,9 @@ const getParentNode = (node) => {
880893
...node,
881894
subSectionIndex: -1
882895
}
883-
} else if (node.sectionIndex !== -1) {
896+
// we shouldn't return parent node with all indexes as `-1`
897+
// that's why if we reach this point and `node.sectionIndex === 0` we should also return `null`
898+
} else if (node.sectionIndex !== -1 && node.sectionIndex !== 0) {
884899
return {
885900
...node,
886901
sectionIndex: -1
@@ -1095,8 +1110,10 @@ const getNodeWhichMustBeUpdatedByCondition = (template, flatProjectData) => {
10951110
}
10961111

10971112
forEachNode(template, (nodeObject, node) => {
1098-
if (nodeObject.condition) {
1099-
const hiddenByCondition = !evaluate(nodeObject.condition, flatProjectData)
1113+
if (nodeObject.condition || nodeObject.userPermissionCondition) {
1114+
// take into account the result of `userPermissionCondition` which we keep in `hiddenByPermission`
1115+
const hiddenByCondition = nodeObject.__wizard.hiddenByPermission
1116+
|| nodeObject.condition && !evaluate(nodeObject.condition, flatProjectData)
11001117

11011118
// only update if the condition result has changed
11021119
if (hiddenByCondition !== nodeObject.__wizard.hiddenByCondition) {
@@ -1194,4 +1211,24 @@ export const buildProjectUpdateQueryByQueryParamSelectCondition = (template, que
11941211
})
11951212

11961213
return updateQuery
1214+
}
1215+
1216+
/**
1217+
* Check if node is visible taking into account parent nodes.
1218+
* If any parent node or node itself is hidden, then the node is treated as hidden.
1219+
*
1220+
* @param {Object} template template
1221+
* @param {Node} node node
1222+
*
1223+
* @returns {Boolean} true if node is visible
1224+
*/
1225+
const isNodeVisible = (template, node) => {
1226+
let isVisible = !_.get(getNodeObject(template, node), '__wizard.hiddenByCondition')
1227+
1228+
let tempNode = node
1229+
while (isVisible && (tempNode = getParentNode(tempNode))) {
1230+
isVisible = isVisible && !_.get(getNodeObject(template, tempNode), '__wizard.hiddenByCondition')
1231+
}
1232+
1233+
return isVisible
11971234
}

src/projects/actions/loadProjects.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ const getProjectsWithMembers = (dispatch, getState, criteria, pageNum) => {
6464
resolve(true)
6565
return dispatch(loadMembers(userIds))
6666
.then(() => resolve(true))
67-
.catch(err => reject(err))
67+
// if some errors happens during members loading this should not break project list loading so we catch error here
68+
.catch(() => {})
6869
})
6970
.catch(err => reject(err))
7071
})

src/projects/detail/components/PortalSubSection.jsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,22 @@ const PortalSubSection = ({
2626
removeAttachment,
2727
canManageAttachments,
2828
attachmentsStorePath,
29+
isCreation,
2930
}) => (
3031
<div>
3132
{content.map(({ sectionIndex }) => {
32-
if (sectionIndex && sectionIndex !== -1 && template && template.sections[sectionIndex]) {
33+
if (sectionIndex !== -1 && template && template.sections[sectionIndex]) {
3334
const section = template.sections[sectionIndex]
35+
36+
if (
37+
// hide if section is hidden by condition
38+
_.get(section, '__wizard.hiddenByCondition') ||
39+
// hide section in edit mode, if it should be hidden on edit
40+
!isCreation && section.hiddenOnEdit
41+
) {
42+
return null
43+
}
44+
3445
return (
3546
<SpecSection
3647
key={'portal-' + (section.id || `section-${sectionIndex}`)}

src/projects/detail/components/ProjectPlanEmpty/ProjectPlanEmpty.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ const ProjectPlanEmpty = ({ isManageUser }) => {
1717
) : (
1818
<div styleName="container">
1919
<h2>Welcome to your project plan</h2>
20-
<p>Thank you for submitting your project requirements. In the next 24h someone from our team will reach out to you to discuss the project details with you so we can build the detailed project plan. Until then stand back and relax, we're working hard on your information.</p>
21-
<p>If you feel like you have more things to send over, or want to reach out to us, please drop us a line at support@topcoder.com. Thanks!</p>
20+
<p>We are reviewing your request. Within the next 24 hours, Topcoder will contact you to discuss next steps, including finalizing your sale and preparing our crowd to meet your needs. Once delivery is mobilized, your project plan will be updated to reflect your detailed delivery plan and will serve as your resource for engaging in key milestones and monitoring progress.</p>
2221
</div>
2322
)
2423
}

src/projects/detail/components/SpecQuestions.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ class SpecQuestions extends React.Component {
401401
(isCreation || !question.hiddenOnEdit)
402402
).map((q, index) => {
403403
return (
404-
_.includes(['checkbox-group', 'radio-group', 'add-ons', 'textinput', 'textbox', 'numberinput', 'skills', 'slide-radiogroup', 'slider-standard', 'select-dropdown'], q.type) && q.visibilityForRendering === STEP_VISIBILITY.READ_OPTIMIZED ? (
404+
_.includes(['checkbox', 'checkbox-group', 'radio-group', 'add-ons', 'textinput', 'textbox', 'numberinput', 'skills', 'slide-radiogroup', 'slider-standard', 'select-dropdown'], q.type) && q.visibilityForRendering === STEP_VISIBILITY.READ_OPTIMIZED ? (
405405
<Accordion
406406
key={q.fieldName || `accordion-${index}`}
407407
title={q.summaryTitle || q.title}

src/projects/detail/components/SpecSection.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,8 @@ const SpecSection = props => {
369369
dirtyProject,
370370
currentWizardStep,
371371
productTemplates,
372-
productCategories
372+
productCategories,
373+
isCreation
373374
}}
374375
/>
375376
)

src/projects/detail/containers/DashboardContainer.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ class DashboardContainer extends React.Component {
272272
) : (
273273
<ProjectPlanEmpty isManageUser={isManageUser} />
274274
)}
275-
{isProjectLive && checkPermission(PERMISSIONS.EDIT_PROJECT_PLAN, project, phases) && !isLoadingPhases && (<div styleName="add-button-container">
275+
{isProjectLive && checkPermission(PERMISSIONS.EDIT_PROJECT_PLAN, project) && !isLoadingPhases && (<div styleName="add-button-container">
276276
<Link to={`/projects/${project.id}/add-phase`} className="tc-btn tc-btn-primary tc-btn-sm action-btn">Add New Phase</Link>
277277
</div>)}
278278
</TwoColsLayout.Content>

0 commit comments

Comments
 (0)