From 8edff13791dece42d1afdc2d1f0e11880f772331 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 1 Sep 2023 07:07:24 +1000 Subject: [PATCH 1/2] WM is not handling error response from submissions-api download endpoint https://topcoder.atlassian.net/browse/PROD-4355 --- .../ChallengeEditor/Submissions/index.js | 74 ++++++++++++++----- src/util/topcoder-react-lib.js | 18 +++++ 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/components/ChallengeEditor/Submissions/index.js b/src/components/ChallengeEditor/Submissions/index.js index 7aba7d92..13428040 100644 --- a/src/components/ChallengeEditor/Submissions/index.js +++ b/src/components/ChallengeEditor/Submissions/index.js @@ -9,6 +9,7 @@ import moment from 'moment' import _ from 'lodash' import { STUDIO_URL, SUBMISSION_REVIEW_APP_URL, getTCMemberURL } from '../../../config/constants' import { PrimaryButton } from '../../Buttons' +import AlertModal from '../../Modal/AlertModal' import cn from 'classnames' import ReactSVG from 'react-svg' import { @@ -20,17 +21,23 @@ import { checkAdmin } from '../../../util/tc' import { - getTopcoderReactLib + getTopcoderReactLib, + isValidDownloadFile } from '../../../util/topcoder-react-lib' import { compressFiles } from '../../../util/files' import styles from './Submissions.module.scss' +import modalStyles from '../../../styles/modal.module.scss' const assets = require.context('../../../assets/images', false, /svg/) const ArrowDown = './arrow-down.svg' const Lock = './lock.svg' const Download = './IconSquareDownload.svg' +const theme = { + container: modalStyles.modalContainer +} + class SubmissionsComponent extends React.Component { constructor (props) { super(props) @@ -42,7 +49,8 @@ class SubmissionsComponent extends React.Component { isShowInformation: false, memberOfModal: '', sortedSubmissions: [], - downloadingAll: false + downloadingAll: false, + alertMessage: '' } this.getSubmissionsSortParam = this.getSubmissionsSortParam.bind(this) this.updateSortedSubmissions = this.updateSortedSubmissions.bind(this) @@ -222,7 +230,7 @@ class SubmissionsComponent extends React.Component { const { field, sort } = this.getSubmissionsSortParam() const revertSort = sort === 'desc' ? 'asc' : 'desc' - const { sortedSubmissions, downloadingAll } = this.state + const { sortedSubmissions, downloadingAll, alertMessage } = this.state const renderSubmission = s => (
@@ -544,19 +552,27 @@ class SubmissionsComponent extends React.Component { const submissionsService = getService(token) submissionsService.downloadSubmission(s.id) .then((blob) => { - // eslint-disable-next-line no-undef - const url = window.URL.createObjectURL(new Blob([blob])) - const link = document.createElement('a') - link.href = url - let fileName = s.legacySubmissionId - if (!fileName) { - fileName = s.id - } - fileName = fileName + '.zip' - link.setAttribute('download', `${fileName}`) - document.body.appendChild(link) - link.click() - link.parentNode.removeChild(link) + isValidDownloadFile(blob).then((isValidFile) => { + if (isValidFile.success) { + // eslint-disable-next-line no-undef + const url = window.URL.createObjectURL(new Blob([blob])) + const link = document.createElement('a') + link.href = url + let fileName = s.legacySubmissionId + if (!fileName) { + fileName = s.id + } + fileName = fileName + '.zip' + link.setAttribute('download', `${fileName}`) + document.body.appendChild(link) + link.click() + link.parentNode.removeChild(link) + } else { + this.setState({ + alertMessage: isValidFile.message || 'Can not download this submission.' + }) + } + }) }) }} > @@ -611,10 +627,14 @@ class SubmissionsComponent extends React.Component { fileName = fileName + '.zip' submissionsService.downloadSubmission(submission.id) .then((blob) => { - const file = new window.File([blob], `${fileName}`) - allFiles.push(file) - downloadedFile += 1 - checkToCompressFiles() + isValidDownloadFile(blob).then((isValidFile) => { + if (isValidFile.success) { + const file = new window.File([blob], `${fileName}`) + allFiles.push(file) + } + downloadedFile += 1 + checkToCompressFiles() + }) }).catch(() => { downloadedFile += 1 checkToCompressFiles() @@ -625,6 +645,20 @@ class SubmissionsComponent extends React.Component {
) : null} + + {alertMessage ? ( + { + this.setState({ + alertMessage: '' + }) + }} + /> + ) : null} ) } diff --git a/src/util/topcoder-react-lib.js b/src/util/topcoder-react-lib.js index 5eb90f84..e139379b 100644 --- a/src/util/topcoder-react-lib.js +++ b/src/util/topcoder-react-lib.js @@ -14,3 +14,21 @@ export const getTopcoderReactLib = () => { const reactLib = require('topcoder-react-lib') return reactLib } + +export const isValidDownloadFile = async (blobFile) => { + if (!blobFile) { + return { + success: false + } + } + if (blobFile.type.indexOf('json') >= 0) { + const backendResonse = JSON.parse(await blobFile.text()) + return { + success: false, + message: backendResonse.message || '' + } + } + return { + success: true + } +} From 6f0bc4c1c04cf4ae32ca8f2b7327f527dc010444 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 14 Sep 2023 09:18:57 +1000 Subject: [PATCH 2/2] Prevent copilots from paying themselves https://github.com/topcoder-platform/work-manager/issues/1568 --- .../ChallengeViewTabs/index.js | 89 +++++++++++++++---- src/components/ChallengeEditor/index.js | 49 +++++++--- src/config/constants.js | 4 + src/util/tc.js | 10 +++ 4 files changed, 119 insertions(+), 33 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeViewTabs/index.js b/src/components/ChallengeEditor/ChallengeViewTabs/index.js index c8cb0007..84ee5676 100644 --- a/src/components/ChallengeEditor/ChallengeViewTabs/index.js +++ b/src/components/ChallengeEditor/ChallengeViewTabs/index.js @@ -13,7 +13,12 @@ import LegacyLinks from '../../LegacyLinks' import ForumLink from '../../ForumLink' import ResourcesTab from '../Resources' import Submissions from '../Submissions' -import { checkAdmin, checkEditResourceRoles, checkReadOnlyRoles } from '../../../util/tc' +import { + checkAdmin, + checkEditResourceRoles, + checkReadOnlyRoles, + checkCopilot +} from '../../../util/tc' import { CHALLENGE_STATUS, MESSAGE } from '../../../config/constants' import Tooltip from '../../Tooltip' import CancelDropDown from '../Cancel-Dropdown' @@ -117,6 +122,21 @@ const ChallengeViewTabs = ({ const isDraft = challenge.status.toUpperCase() === CHALLENGE_STATUS.DRAFT const isSelfServiceCopilot = challenge.legacy.selfServiceCopilot === loggedInUser.handle const isAdmin = checkAdmin(token) + + // Make sure that the Launch and Mark as completed buttons are hidden + // for tasks that are assigned to the current logged in user, if that user has the copilot role. + const preventCopilotFromActivatingTask = useMemo(() => { + return isTask && + checkCopilot(token) && + assignedMemberDetails && + loggedInUser && + `${loggedInUser.userId}` === `${assignedMemberDetails.userId}` + }, [ + token, + assignedMemberDetails, + loggedInUser + ]) + const isReadOnly = checkReadOnlyRoles(token) const canApprove = (isSelfServiceCopilot || enableEdit) && isDraft && isSelfService const hasBillingAccount = _.get(projectDetail, 'billingAccountId') !== null @@ -125,10 +145,35 @@ const ChallengeViewTabs = ({ // OR if this isn't a non-self-service draft, permit launching if: // a) the current user is either the self-service copilot or is an admin AND // b) the challenge is approved - const canLaunch = enableEdit && hasBillingAccount && !isReadOnly && - ((!isSelfService && isDraft) || - ((isSelfServiceCopilot || isAdmin) && - challenge.status.toUpperCase() === CHALLENGE_STATUS.APPROVED)) + const canLaunch = useMemo(() => { + return enableEdit && + hasBillingAccount && + (!isReadOnly) && + (!preventCopilotFromActivatingTask) && + ( + ( + !isSelfService && + isDraft + ) || + ( + ( + isSelfServiceCopilot || + isAdmin + ) && + challenge.status.toUpperCase() === CHALLENGE_STATUS.APPROVED + ) + ) + }, [ + enableEdit, + hasBillingAccount, + isReadOnly, + isSelfService, + isDraft, + isSelfServiceCopilot, + isAdmin, + challenge.status, + preventCopilotFromActivatingTask + ]) return (
@@ -184,20 +229,26 @@ const ChallengeViewTabs = ({ />
)} - {isTask && challenge.status === 'Active' && ( -
- {assignedMemberDetails ? ( - - - - ) : ( - - {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} - - - )} -
- )} + { + ( + isTask && + challenge.status === 'Active' && + !preventCopilotFromActivatingTask + ) && ( +
+ {assignedMemberDetails ? ( + + + + ) : ( + + {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} + + + )} +
+ ) + } {enableEdit && !canEditResource && ( )} diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index f181e87b..0bf42dfa 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -30,7 +30,12 @@ import { MULTI_ROUND_CHALLENGE_TEMPLATE_ID, DS_TRACK_ID, CHALLENGE_STATUS } from '../../config/constants' -import { getDomainTypes, getResourceRoleByName, is2RoundsChallenge } from '../../util/tc' +import { + getDomainTypes, + getResourceRoleByName, + is2RoundsChallenge, + checkCopilot +} from '../../util/tc' import { getPhaseEndDate } from '../../util/date' import { PrimaryButton, OutlineButton } from '../Buttons' import TrackField from './Track-Field' @@ -1527,6 +1532,14 @@ class ChallengeEditor extends Component { const statusMessage = challenge.status && challenge.status.split(' ')[0].toUpperCase() const errorContainer =
{error}
+ // Make sure that the Launch and Mark as completed buttons are hidden + // for tasks that are assigned to the current logged in user, if that user has the copilot role. + const preventCopilotFromActivatingTask = isTask && + checkCopilot(token) && + assignedMemberDetails && + loggedInUser && + `${loggedInUser.userId}` === `${assignedMemberDetails.userId}` + const actionButtons = {!isLoading && this.state.hasValidationErrors &&
Please fix the errors before saving
} { @@ -1553,18 +1566,23 @@ class ChallengeEditor extends Component { )} - {isDraft && ( -
- {(challenge.legacyId || isTask) && !this.state.hasValidationErrors ? ( - - ) : ( - - {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} - - - )} -
- )} + { + ( + isDraft && + !preventCopilotFromActivatingTask + ) && ( +
+ {(challenge.legacyId || isTask) && !this.state.hasValidationErrors ? ( + + ) : ( + + {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} + + + )} +
+ ) + } {statusMessage !== CHALLENGE_STATUS.CANCELLED &&
@@ -1575,7 +1593,10 @@ class ChallengeEditor extends Component {
- {isTask && ( + {( + isTask && + !preventCopilotFromActivatingTask + ) && (
diff --git a/src/config/constants.js b/src/config/constants.js index 0bb1db0f..a959f65b 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -263,6 +263,10 @@ export const ADMIN_ROLES = [ 'connect admin' ] +export const COPILOT_ROLES = [ + 'copilot' +] + export const downloadAttachmentURL = (challengeId, attachmentId, token) => `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}/download?token=${token}` diff --git a/src/util/tc.js b/src/util/tc.js index 2f85ed5d..00b8c81e 100644 --- a/src/util/tc.js +++ b/src/util/tc.js @@ -6,6 +6,7 @@ import { CHALLENGE_TRACKS, ALLOWED_USER_ROLES, ADMIN_ROLES, + COPILOT_ROLES, SUBMITTER_ROLE_UUID, READ_ONLY_ROLES, ALLOWED_DOWNLOAD_SUBMISSIONS_ROLES, @@ -198,6 +199,15 @@ export const checkAdmin = token => { return roles.some(val => ADMIN_ROLES.indexOf(val.toLowerCase()) > -1) } +/** + * Checks if token has any of the copilot roles + * @param token + */ +export const checkCopilot = token => { + const roles = _.get(decodeToken(token), 'roles') + return roles.some(val => COPILOT_ROLES.indexOf(val.toLowerCase()) > -1) +} + /** * Get resource role by name *