From caf0a6a34364d0c09498d6ad5d1e682329eed150 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 15 Dec 2025 23:58:50 +0100 Subject: [PATCH 01/13] fix: prevent download for submitters if challenge is configured that way --- .../TabContentSubmissions.tsx | 30 +++++++------- .../components/TableAppeals/TableAppeals.tsx | 37 ++++++++++++++++- .../TableCheckpointSubmissions.tsx | 41 +++++++++++++++++-- .../TableIterativeReview.tsx | 40 ++++++++++++++++-- .../components/TableReview/TableReview.tsx | 36 +++++++++++++++- .../TableReviewForSubmitter.tsx | 22 +++++----- .../TableSubmissionScreening.tsx | 37 ++++++++++++++++- .../components/TableWinners/TableWinners.tsx | 28 ++++++------- .../common/TableColumnRenderers.tsx | 10 ++++- .../review/src/lib/components/common/types.ts | 2 + src/apps/review/src/lib/constants.ts | 2 + 11 files changed, 230 insertions(+), 55 deletions(-) diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index bacfe9dba..387b78157 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -44,6 +44,7 @@ import type { SubmissionHistoryPartition } from '../../utils' import { TABLE_DATE_FORMAT } from '../../../config/index.config' import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow' import { useRolePermissions, UseRolePermissionsResult } from '../../hooks' +import { SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE } from '../../constants' import styles from './TabContentSubmissions.module.scss' @@ -234,24 +235,13 @@ export const TabContentSubmissions: FC = props => { const filteredSubmissions = useMemo( () => { - - const filterFunc = (submissions: BackendSubmission[]): BackendSubmission[] => submissions - .filter(submission => { - if (!canViewSubmissions) { - return String(submission.memberId) === String(loginUserInfo?.userId) - } - - return true - }) - const filteredByUserId = filterFunc(latestBackendSubmissions) - const filteredByUserIdSubmissions = filterFunc(props.submissions) if (restrictToLatest && hasLatestFlag) { return latestBackendSubmissions.length - ? filteredByUserId - : filteredByUserIdSubmissions + ? latestBackendSubmissions + : props.submissions } - return filteredByUserIdSubmissions + return props.submissions }, [ latestBackendSubmissions, @@ -280,13 +270,21 @@ export const TabContentSubmissions: FC = props => { ? undefined : submission.virusScan const failedScan = normalizedVirusScan === false - const isRestricted = isRestrictedBase || failedScan - const tooltipMessage = failedScan + const canDownloadSubmission = ( + !canViewSubmissions && String(submission.memberId) === String(loginUserInfo?.userId) + ) + const isRestricted = isRestrictedBase || failedScan || !canDownloadSubmission + let tooltipMessage = failedScan ? VIRUS_SCAN_FAILED_MESSAGE : ( getRestrictionMessageForMember(submission.memberId) ?? restrictionMessage ) + + if (!canDownloadSubmission) { + tooltipMessage = SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE + } + const isButtonDisabled = Boolean( props.isDownloading[submission.id] || isRestricted, diff --git a/src/apps/review/src/lib/components/TableAppeals/TableAppeals.tsx b/src/apps/review/src/lib/components/TableAppeals/TableAppeals.tsx index 69bd760e0..a4415fa77 100644 --- a/src/apps/review/src/lib/components/TableAppeals/TableAppeals.tsx +++ b/src/apps/review/src/lib/components/TableAppeals/TableAppeals.tsx @@ -13,7 +13,7 @@ import { useWindowSize, WindowSize } from '~/libs/shared' import { Table, TableColumn } from '~/libs/ui' import { WITHOUT_APPEAL } from '../../../config/index.config' -import { ChallengeDetailContext } from '../../contexts' +import { ChallengeDetailContext, ReviewAppContext } from '../../contexts' import { useSubmissionDownloadAccess } from '../../hooks/useSubmissionDownloadAccess' import type { UseSubmissionDownloadAccessResult } from '../../hooks/useSubmissionDownloadAccess' import { useRolePermissions } from '../../hooks/useRolePermissions' @@ -22,6 +22,7 @@ import { ChallengeDetailContextModel, ChallengeInfo, MappingReviewAppeal, + ReviewAppContextModel, SubmissionInfo, } from '../../models' import { TableWrapper } from '../TableWrapper' @@ -70,7 +71,7 @@ export const TableAppeals: FC = (props: TableAppealsProps) => reviewers, }: ChallengeDetailContextModel = useContext(ChallengeDetailContext) const { width: screenWidth }: WindowSize = useWindowSize() - + const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext) const downloadAccess: UseSubmissionDownloadAccessResult = useSubmissionDownloadAccess() const { getRestrictionMessageForMember, @@ -219,6 +220,37 @@ export const TableAppeals: FC = (props: TableAppealsProps) => [aggregatedResults], ) + const { canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions() + + const isCompletedDesignChallenge = useMemo(() => { + if (!challengeInfo) return false + const type = challengeInfo.track.name ? String(challengeInfo.track.name) + .toLowerCase() : '' + const status = challengeInfo.status ? String(challengeInfo.status) + .toLowerCase() : '' + return type === 'design' && ( + status === 'completed' + ) + }, [challengeInfo]) + + const isSubmissionsViewable = useMemo(() => { + if (!challengeInfo?.metadata?.length) return false + return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value) + .toLowerCase() === 'true') + }, [challengeInfo]) + + const canViewSubmissions = useMemo(() => { + if (isCompletedDesignChallenge) { + return canViewAllSubmissions || isSubmissionsViewable + } + + return true + }, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions]) + + const isSubmissionNotViewable = (submission: SubmissionRow): boolean => ( + !canViewSubmissions && String(submission.memberId) !== String(loginUserInfo?.userId) + ) + const downloadButtonConfig = useMemo( () => ({ downloadSubmission, @@ -226,6 +258,7 @@ export const TableAppeals: FC = (props: TableAppealsProps) => isDownloading, isSubmissionDownloadRestricted, isSubmissionDownloadRestrictedForMember, + isSubmissionNotViewable, ownedMemberIds, restrictionMessage, shouldRestrictSubmitterToOwnSubmission: false, diff --git a/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx b/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx index 4af5d7198..08a606372 100644 --- a/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx +++ b/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx @@ -32,9 +32,10 @@ import { import { ChallengeDetailContext, ReviewAppContext } from '../../contexts' import { updateReview } from '../../services' import { ConfirmModal } from '../ConfirmModal' -import { useSubmissionDownloadAccess } from '../../hooks' +import { useRolePermissions, UseRolePermissionsResult, useSubmissionDownloadAccess } from '../../hooks' import type { UseSubmissionDownloadAccessResult } from '../../hooks/useSubmissionDownloadAccess' import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow' +import { SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE } from '../../constants' import styles from './TableCheckpointSubmissions.module.scss' @@ -65,6 +66,7 @@ export const TableCheckpointSubmissions: FC = (props: Props) => { const datas: Screening[] | undefined = props.datas const downloadSubmission = props.downloadSubmission const isDownloading = props.isDownloading + const { canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions() const { challengeInfo, @@ -115,6 +117,31 @@ export const TableCheckpointSubmissions: FC = (props: Props) => { getRestrictionMessageForMember, }: UseSubmissionDownloadAccessResult = useSubmissionDownloadAccess() + const isCompletedDesignChallenge = useMemo(() => { + if (!challengeInfo) return false + const type = challengeInfo.track.name ? String(challengeInfo.track.name) + .toLowerCase() : '' + const status = challengeInfo.status ? String(challengeInfo.status) + .toLowerCase() : '' + return type === 'design' && ( + status === 'completed' + ) + }, [challengeInfo]) + + const isSubmissionsViewable = useMemo(() => { + if (!challengeInfo?.metadata?.length) return false + return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value) + .toLowerCase() === 'true') + }, [challengeInfo]) + + const canViewSubmissions = useMemo(() => { + if (isCompletedDesignChallenge) { + return canViewAllSubmissions || isSubmissionsViewable + } + + return true + }, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions]) + const openReopenDialog = useCallback( (entry: Screening, isOwnReview: boolean): void => { if (!entry.reviewId) { @@ -211,10 +238,18 @@ export const TableCheckpointSubmissions: FC = (props: Props) => { ? undefined : data.virusScan const failedScan = normalizedVirusScan === false - const isRestrictedForRow = isRestrictedBase || failedScan - const tooltipMessage = failedScan + const isDownloadDisabled = ( + !canViewSubmissions && String(data.memberId) !== String(loginUserInfo?.userId) + ) + const isRestrictedForRow = isRestrictedBase || failedScan || isDownloadDisabled + let tooltipMessage = failedScan ? 'Submission failed virus scan' : (getRestrictionMessageForMember(data.memberId) ?? restrictionMessage) + + if (isDownloadDisabled) { + tooltipMessage = SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE + } + const isButtonDisabled = Boolean( isDownloading[data.submissionId] || isRestrictedForRow, diff --git a/src/apps/review/src/lib/components/TableIterativeReview/TableIterativeReview.tsx b/src/apps/review/src/lib/components/TableIterativeReview/TableIterativeReview.tsx index 68e4a9ce8..2208a9a1c 100644 --- a/src/apps/review/src/lib/components/TableIterativeReview/TableIterativeReview.tsx +++ b/src/apps/review/src/lib/components/TableIterativeReview/TableIterativeReview.tsx @@ -45,6 +45,7 @@ import { resolveSubmissionReviewResult } from '../common/reviewResult' import { ProgressBar } from '../ProgressBar' import { TableWrapper } from '../TableWrapper' import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow' +import { SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE } from '../../constants' import styles from './TableIterativeReview.module.scss' @@ -505,7 +506,7 @@ export const TableIterativeReview: FC = (props: Props) => { const isTablet = useMemo(() => screenWidth <= 744, [screenWidth]) const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext) const { actionChallengeRole, myChallengeResources }: useRoleProps = useRole() - const { isCopilotWithReviewerAssignments }: UseRolePermissionsResult = useRolePermissions() + const { isCopilotWithReviewerAssignments, canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions() const isSubmitterView = actionChallengeRole === SUBMITTER const ownedMemberIds: Set = useMemo( (): Set => new Set( @@ -658,6 +659,31 @@ export const TableIterativeReview: FC = (props: Props) => { const isPostMortemColumn = columnLabelKey === 'postmortem' const isApprovalColumn = columnLabelKey === 'approval' + const isCompletedDesignChallenge = useMemo(() => { + if (!challengeInfo) return false + const type = challengeInfo.track.name ? String(challengeInfo.track.name) + .toLowerCase() : '' + const status = challengeInfo.status ? String(challengeInfo.status) + .toLowerCase() : '' + return type === 'design' && ( + status === 'completed' + ) + }, [challengeInfo]) + + const isSubmissionsViewable = useMemo(() => { + if (!challengeInfo?.metadata?.length) return false + return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value) + .toLowerCase() === 'true') + }, [challengeInfo]) + + const canViewSubmissions = useMemo(() => { + if (isCompletedDesignChallenge) { + return canViewAllSubmissions || isSubmissionsViewable + } + + return true + }, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions]) + const submissionColumn: TableColumn = useMemo( () => ({ className: styles.submissionColumn, @@ -682,10 +708,14 @@ export const TableIterativeReview: FC = (props: Props) => { ? undefined : data.virusScan const failedScan = normalizedVirusScan === false + const isDownloadDisabled = ( + !canViewSubmissions && String(data.memberId) !== String(loginUserInfo?.userId) + ) const isButtonDisabled = Boolean( isDownloading[data.id] || isRestrictedForMember - || failedScan, + || failedScan + || isDownloadDisabled, ) const downloadButton = ( @@ -712,7 +742,7 @@ export const TableIterativeReview: FC = (props: Props) => { }) } - const tooltipContent = failedScan + let tooltipContent = failedScan ? 'Submission failed virus scan' : isRestrictedForMember ? memberRestrictionMessage ?? restrictionMessage @@ -720,6 +750,10 @@ export const TableIterativeReview: FC = (props: Props) => { ? DOWNLOAD_OWN_SUBMISSION_TOOLTIP : (isSubmissionDownloadRestricted && restrictionMessage) || undefined + if (isDownloadDisabled) { + tooltipContent = SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE + } + const downloadControl = isOwnershipRestricted ? ( {data.id} diff --git a/src/apps/review/src/lib/components/TableReview/TableReview.tsx b/src/apps/review/src/lib/components/TableReview/TableReview.tsx index 2635577bb..cbb6bf83d 100644 --- a/src/apps/review/src/lib/components/TableReview/TableReview.tsx +++ b/src/apps/review/src/lib/components/TableReview/TableReview.tsx @@ -17,7 +17,7 @@ import { MobileTableColumn } from '~/apps/admin/src/lib/models/MobileTableColumn import { handleError, useWindowSize, WindowSize } from '~/libs/shared' import { IconOutline, Table, TableColumn } from '~/libs/ui' -import { ChallengeDetailContext } from '../../contexts' +import { ChallengeDetailContext, ReviewAppContext } from '../../contexts' import { useRole, useScorecardPassingScores, useSubmissionDownloadAccess } from '../../hooks' import type { useRoleProps } from '../../hooks/useRole' import { useSubmissionHistory } from '../../hooks/useSubmissionHistory' @@ -28,6 +28,7 @@ import type { UseSubmissionDownloadAccessResult } from '../../hooks/useSubmissio import { ChallengeDetailContextModel, MappingReviewAppeal, + ReviewAppContextModel, SubmissionInfo, } from '../../models' import { @@ -120,6 +121,7 @@ export const TableReview: FC = (props: TableReviewProps) => { isSubmissionDownloadRestrictedForMember, restrictionMessage, }: UseSubmissionDownloadAccessResult = useSubmissionDownloadAccess() + const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext) const isTablet = useMemo(() => screenWidth <= 744, [screenWidth]) const reviewPhaseDatas = useMemo( @@ -379,6 +381,37 @@ export const TableReview: FC = (props: TableReviewProps) => { [], ) + const { canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions() + + const isCompletedDesignChallenge = useMemo(() => { + if (!challengeInfo) return false + const type = challengeInfo.track.name ? String(challengeInfo.track.name) + .toLowerCase() : '' + const status = challengeInfo.status ? String(challengeInfo.status) + .toLowerCase() : '' + return type === 'design' && ( + status === 'completed' + ) + }, [challengeInfo]) + + const isSubmissionsViewable = useMemo(() => { + if (!challengeInfo?.metadata?.length) return false + return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value) + .toLowerCase() === 'true') + }, [challengeInfo]) + + const canViewSubmissions = useMemo(() => { + if (isCompletedDesignChallenge) { + return canViewAllSubmissions || isSubmissionsViewable + } + + return true + }, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions]) + + const isSubmissionNotViewable = (submission: SubmissionRow): boolean => ( + !canViewSubmissions && String(submission.memberId) !== String(loginUserInfo?.userId) + ) + const downloadButtonConfig = useMemo( () => ({ downloadSubmission, @@ -386,6 +419,7 @@ export const TableReview: FC = (props: TableReviewProps) => { isDownloading, isSubmissionDownloadRestricted, isSubmissionDownloadRestrictedForMember, + isSubmissionNotViewable, ownedMemberIds, restrictionMessage, shouldRestrictSubmitterToOwnSubmission: false, diff --git a/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx b/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx index 9f37c1c73..adc1299ac 100644 --- a/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx +++ b/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx @@ -310,21 +310,16 @@ export const TableReviewForSubmitter: FC = (props: [mappingReviewAppeal, reviewers, submissionsForAggregation], ) - const filterFunc = useCallback((submissions: SubmissionRow[]): SubmissionRow[] => submissions - .filter(submission => { - if (!canViewSubmissions) { - return String(submission.memberId) === String(loginUserInfo?.userId) - } - - return true - }), [canViewSubmissions, loginUserInfo?.userId]) + const isSubmissionNotViewable = (submission: SubmissionRow): boolean => ( + !canViewSubmissions && String(submission.memberId) !== String(loginUserInfo?.userId) + ) const aggregatedSubmissionRows = useMemo( - () => filterFunc(aggregatedRows.map(row => ({ + () => aggregatedRows.map(row => ({ ...row.submission, aggregated: row, - }))), - [aggregatedRows, filterFunc, canViewSubmissions, loginUserInfo?.userId], + })), + [aggregatedRows, canViewSubmissions, loginUserInfo?.userId], ) const scorecardIds = useMemo>(() => { @@ -371,8 +366,9 @@ export const TableReviewForSubmitter: FC = (props: downloadSubmission, getRestrictionMessageForMember, isDownloading, - isSubmissionDownloadRestricted, + isSubmissionDownloadRestricted: isSubmissionDownloadRestricted || (!canViewSubmissions && !isOwned), isSubmissionDownloadRestrictedForMember, + isSubmissionNotViewable, ownedMemberIds, restrictionMessage, shouldRestrictSubmitterToOwnSubmission, @@ -382,9 +378,11 @@ export const TableReviewForSubmitter: FC = (props: isDownloading, isSubmissionDownloadRestricted, isSubmissionDownloadRestrictedForMember, + isSubmissionNotViewable, ownedMemberIds, restrictionMessage, shouldRestrictSubmitterToOwnSubmission, + canViewSubmissions, ]) const columns = useMemo[]>(() => { diff --git a/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx b/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx index f09cc304b..027d204e6 100644 --- a/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx +++ b/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx @@ -45,7 +45,7 @@ import { import { ChallengeDetailContext, ReviewAppContext } from '../../contexts' import { updateReview } from '../../services' import { ConfirmModal } from '../ConfirmModal' -import { useRole, useSubmissionDownloadAccess } from '../../hooks' +import { useRole, useRolePermissions, UseRolePermissionsResult, useSubmissionDownloadAccess } from '../../hooks' import type { UseSubmissionDownloadAccessResult } from '../../hooks/useSubmissionDownloadAccess' import type { useRoleProps } from '../../hooks/useRole' import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow' @@ -69,6 +69,7 @@ interface SubmissionColumnConfig { getRestrictionMessageForMember: (memberId?: string) => string | undefined isDownloading: IsRemovingType isSubmissionDownloadRestrictedForMember: (memberId?: string) => boolean + isSubmissionNotViewable: (submission: Screening) => boolean restrictionMessage?: string } @@ -132,7 +133,7 @@ const createSubmissionColumn = (config: SubmissionColumnConfig): TableColumn = (props: Props) => { ), [props.screenings], ) + const { canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions() + + const isCompletedDesignChallenge = useMemo(() => { + if (!challengeInfo) return false + const type = challengeInfo.track.name ? String(challengeInfo.track.name) + .toLowerCase() : '' + const status = challengeInfo.status ? String(challengeInfo.status) + .toLowerCase() : '' + return type === 'design' && ( + status === 'completed' + ) + }, [challengeInfo]) + + const isSubmissionsViewable = useMemo(() => { + if (!challengeInfo?.metadata?.length) return false + return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value) + .toLowerCase() === 'true') + }, [challengeInfo]) + + const canViewSubmissions = useMemo(() => { + if (isCompletedDesignChallenge) { + return canViewAllSubmissions || isSubmissionsViewable + } + + return true + }, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions]) const filteredChallengeSubmissions = useMemo( () => { @@ -1000,12 +1027,17 @@ export const TableSubmissionScreening: FC = (props: Props) => { (submissionId: string): SubmissionInfo | undefined => submissionMetaById.get(submissionId), [submissionMetaById], ) + + const isSubmissionNotViewable = (submission: Screening): boolean => ( + !canViewSubmissions && String(submission.memberId) !== String(loginUserInfo?.userId) + ) const submissionColumn = useMemo( () => createSubmissionColumn({ downloadSubmission: props.downloadSubmission, getRestrictionMessageForMember, isDownloading: props.isDownloading, isSubmissionDownloadRestrictedForMember, + isSubmissionNotViewable, restrictionMessage, }), [ @@ -1013,6 +1045,7 @@ export const TableSubmissionScreening: FC = (props: Props) => { props.isDownloading, getRestrictionMessageForMember, isSubmissionDownloadRestrictedForMember, + isSubmissionNotViewable, restrictionMessage, ], ) diff --git a/src/apps/review/src/lib/components/TableWinners/TableWinners.tsx b/src/apps/review/src/lib/components/TableWinners/TableWinners.tsx index 847792586..2bfdcf945 100644 --- a/src/apps/review/src/lib/components/TableWinners/TableWinners.tsx +++ b/src/apps/review/src/lib/components/TableWinners/TableWinners.tsx @@ -1,7 +1,7 @@ /** * Table Winners. */ -import { FC, useCallback, useContext, useMemo } from 'react' +import { FC, useContext, useMemo } from 'react' import { Link, useLocation } from 'react-router-dom' import _ from 'lodash' import classNames from 'classnames' @@ -25,6 +25,7 @@ import type { PhaseOrderingOptions } from '../../utils' import { useRolePermissions, UseRolePermissionsResult, useSubmissionDownloadAccess } from '../../hooks' import type { UseSubmissionDownloadAccessResult } from '../../hooks/useSubmissionDownloadAccess' import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow' +import { SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE } from '../../constants' import styles from './TableWinners.module.scss' @@ -84,17 +85,6 @@ export const TableWinners: FC = (props: Props) => { return true }, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions]) - const filterFunc = useCallback((submissions: ProjectResult[]): ProjectResult[] => submissions - .filter(submission => { - if (!canViewSubmissions) { - return String(submission.userId) === String(loginUserInfo?.userId) - } - - return true - }), [canViewSubmissions, loginUserInfo?.userId]) - - const winnerData = filterFunc(datas) - const reviewTabUrl = useMemo(() => { const searchParams = new URLSearchParams(location.search) const challengePhases = challengeInfo?.phases ?? [] @@ -146,11 +136,19 @@ export const TableWinners: FC = (props: Props) => { label: 'Submission ID', propertyName: 'submissionId', renderer: (data: ProjectResult) => { + const cannotDownloadSubmission = ( + !canViewSubmissions && String(data.userId) !== String(loginUserInfo?.userId) + ) const isRestrictedForRow = data.submissionId ? isSubmissionDownloadRestrictedForMember(data.userId) + || cannotDownloadSubmission : false - const tooltipMessage = getRestrictionMessageForMember(data.userId) + let tooltipMessage = getRestrictionMessageForMember(data.userId) ?? restrictionMessage + if (cannotDownloadSubmission) { + tooltipMessage = SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE + } + const isButtonDisabled = Boolean( (data.submissionId ? isDownloading[data.submissionId] @@ -310,11 +308,11 @@ export const TableWinners: FC = (props: Props) => { )} > {isTablet ? ( - + ) : ( {submission.id} diff --git a/src/apps/review/src/lib/components/common/types.ts b/src/apps/review/src/lib/components/common/types.ts index 0d56e70ae..667f987a3 100644 --- a/src/apps/review/src/lib/components/common/types.ts +++ b/src/apps/review/src/lib/components/common/types.ts @@ -59,6 +59,8 @@ export interface DownloadButtonConfig { virusScanFailedMessage?: string /** Tooltip used when a user is limited to downloading their own submission. */ downloadOwnSubmissionTooltip?: string + /** Method to determine whether a submission is viewable. */ + isSubmissionNotViewable?: (submission: SubmissionRow) => boolean } /** diff --git a/src/apps/review/src/lib/constants.ts b/src/apps/review/src/lib/constants.ts index b03395e68..4d04d03ce 100644 --- a/src/apps/review/src/lib/constants.ts +++ b/src/apps/review/src/lib/constants.ts @@ -1,3 +1,5 @@ export const SUBMISSION_TYPE_CONTEST = 'CONTEST_SUBMISSION' export const SUBMISSION_TYPE_CHECKPOINT = 'CHECKPOINT_SUBMISSION' export const TABLE_DATE_FORMAT = 'MMM DD, HH:mm A' +export const SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE + = 'This challenge is a private challenge. You do not have permission to download submissions.' From caeaa4f4e82dc7ad58ea8c5fff5d96c2b2d1118b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 18 Dec 2025 23:36:16 +0100 Subject: [PATCH 02/13] fix: review comments --- .../ChallengeDetailsContent/TabContentSubmissions.tsx | 6 +++--- .../review/src/lib/components/TableReview/TableReview.tsx | 1 + .../TableReviewForSubmitter/TableReviewForSubmitter.tsx | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index 387b78157..e4af31189 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -270,10 +270,10 @@ export const TabContentSubmissions: FC = props => { ? undefined : submission.virusScan const failedScan = normalizedVirusScan === false - const canDownloadSubmission = ( + const cannotDownloadSubmission = ( !canViewSubmissions && String(submission.memberId) === String(loginUserInfo?.userId) ) - const isRestricted = isRestrictedBase || failedScan || !canDownloadSubmission + const isRestricted = isRestrictedBase || failedScan || !cannotDownloadSubmission let tooltipMessage = failedScan ? VIRUS_SCAN_FAILED_MESSAGE : ( @@ -281,7 +281,7 @@ export const TabContentSubmissions: FC = props => { ?? restrictionMessage ) - if (!canDownloadSubmission) { + if (!cannotDownloadSubmission) { tooltipMessage = SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE } diff --git a/src/apps/review/src/lib/components/TableReview/TableReview.tsx b/src/apps/review/src/lib/components/TableReview/TableReview.tsx index 99714b8a8..eea1146e6 100644 --- a/src/apps/review/src/lib/components/TableReview/TableReview.tsx +++ b/src/apps/review/src/lib/components/TableReview/TableReview.tsx @@ -430,6 +430,7 @@ export const TableReview: FC = (props: TableReviewProps) => { isDownloading, isSubmissionDownloadRestricted, isSubmissionDownloadRestrictedForMember, + isSubmissionNotViewable, ownedMemberIds, restrictionMessage, ], diff --git a/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx b/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx index adc1299ac..fd161e7dc 100644 --- a/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx +++ b/src/apps/review/src/lib/components/TableReviewForSubmitter/TableReviewForSubmitter.tsx @@ -319,7 +319,7 @@ export const TableReviewForSubmitter: FC = (props: ...row.submission, aggregated: row, })), - [aggregatedRows, canViewSubmissions, loginUserInfo?.userId], + [aggregatedRows], ) const scorecardIds = useMemo>(() => { @@ -366,7 +366,7 @@ export const TableReviewForSubmitter: FC = (props: downloadSubmission, getRestrictionMessageForMember, isDownloading, - isSubmissionDownloadRestricted: isSubmissionDownloadRestricted || (!canViewSubmissions && !isOwned), + isSubmissionDownloadRestricted, isSubmissionDownloadRestrictedForMember, isSubmissionNotViewable, ownedMemberIds, From 6b554a4302459ad2ecfe69aa70b643e6b3337ffb Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 10 Feb 2026 15:24:14 +0200 Subject: [PATCH 03/13] PM-3717 - map challenge role to clear labels --- .../shared/lib/components/skill-pill/SkillPill.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libs/shared/lib/components/skill-pill/SkillPill.tsx b/src/libs/shared/lib/components/skill-pill/SkillPill.tsx index ebb021343..9f7266d77 100644 --- a/src/libs/shared/lib/components/skill-pill/SkillPill.tsx +++ b/src/libs/shared/lib/components/skill-pill/SkillPill.tsx @@ -21,6 +21,15 @@ export interface SkillPillProps { fetchSkillDetails?: (skillId: string) => Promise } +const skillEventTypeMap = { + challenge_win: 'Win', + challenge_2nd_place: 'Win', + challenge_3rd_place: 'Win', + challenge_finisher: 'Submit', + challenge_copilot: 'Copilot', + challenge_review: 'Review', +} + const SkillPill: FC = props => { const [hideDetails, setHideDetails] = useState(false) const [loadDetails, setLoadDetails] = useState(false) @@ -79,7 +88,7 @@ const SkillPill: FC = props => {
Challenges - {' '} - {role} + {skillEventTypeMap[role as keyof typeof skillEventTypeMap] ?? ''} {' '} ( {skillDetails.activity.challenge![role].count} From 7e2e4cf8e3a7ad9e32cbcfcf52e45ea1975f6f4b Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 10 Feb 2026 15:30:55 +0200 Subject: [PATCH 04/13] lint --- src/libs/shared/lib/components/skill-pill/SkillPill.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/shared/lib/components/skill-pill/SkillPill.tsx b/src/libs/shared/lib/components/skill-pill/SkillPill.tsx index 9f7266d77..9cbb0f5c0 100644 --- a/src/libs/shared/lib/components/skill-pill/SkillPill.tsx +++ b/src/libs/shared/lib/components/skill-pill/SkillPill.tsx @@ -22,12 +22,12 @@ export interface SkillPillProps { } const skillEventTypeMap = { - challenge_win: 'Win', challenge_2nd_place: 'Win', challenge_3rd_place: 'Win', - challenge_finisher: 'Submit', challenge_copilot: 'Copilot', + challenge_finisher: 'Submit', challenge_review: 'Review', + challenge_win: 'Win', } const SkillPill: FC = props => { From 5bbdaa588b1a79c32a5f5e8caf88a09ccc036d88 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 12 Feb 2026 07:52:24 +1100 Subject: [PATCH 05/13] Hide emails except if the user is viewing their own profile (PS-526) --- src/apps/profiles/src/member-profile/phones/Phones.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/profiles/src/member-profile/phones/Phones.tsx b/src/apps/profiles/src/member-profile/phones/Phones.tsx index 3c87d2f33..f94cd186f 100644 --- a/src/apps/profiles/src/member-profile/phones/Phones.tsx +++ b/src/apps/profiles/src/member-profile/phones/Phones.tsx @@ -21,6 +21,7 @@ interface PhonesProps { const Phones: FC = (props: PhonesProps) => { const canEdit: boolean = props.authProfile?.handle === props.profile.handle const canSeePhonesValue: boolean = canSeePhones(props.authProfile, props.profile) + const canSeeEmail: boolean = props.authProfile?.userId === props.profile.userId const [isEditMode, setIsEditMode]: [boolean, Dispatch>] = useState(false) @@ -61,7 +62,7 @@ const Phones: FC = (props: PhonesProps) => { )}

- {props.profile?.email && ( + {canSeeEmail && props.profile?.email && (
From affce2a94dc2fd13d22a972559df172f28854fc4 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 12 Feb 2026 09:47:24 +1100 Subject: [PATCH 06/13] Allow talent manager, product manager, and admins ability to see emails on profiles. --- src/apps/profiles/src/config/constants.ts | 6 ++++ src/apps/profiles/src/lib/helpers.ts | 28 ++++++++++++++++++- .../src/member-profile/phones/Phones.tsx | 6 ++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/apps/profiles/src/config/constants.ts b/src/apps/profiles/src/config/constants.ts index 9122a1dcd..6d975e532 100644 --- a/src/apps/profiles/src/config/constants.ts +++ b/src/apps/profiles/src/config/constants.ts @@ -37,3 +37,9 @@ export const PHONE_NUMBER_ROLES = [ UserRole.projectManager, UserRole.copilot, ] + +export const EMAIL_VIEW_ROLES = [ + UserRole.administrator, + UserRole.talentManager, + UserRole.projectManager, +] diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index f6e3e719c..627d4a29c 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -2,7 +2,7 @@ import { UserProfile, UserRole, UserTrait } from '~/libs/core' import { availabilityOptions, preferredRoleOptions } from '~/libs/shared/lib/components/modify-open-to-work-modal' -import { ADMIN_ROLES, PHONE_NUMBER_ROLES } from '../config' +import { ADMIN_ROLES, EMAIL_VIEW_ROLES, PHONE_NUMBER_ROLES } from '../config' declare global { interface Window { tcUniNav: any } @@ -193,6 +193,32 @@ export function canSeePhones(authProfile: UserProfile | undefined, profile: User return false } +/** + * Check if the user can see email address + * @param authProfile - The authenticated user profile + * @param profile - The profile to check if the user can see email + * @returns {boolean} - Whether the user can see email + */ +export function canSeeEmail(authProfile: UserProfile | undefined, profile: UserProfile): boolean { + if (!authProfile) { + return false + } + + if (authProfile.handle === profile.handle) { + return true + } + + if (authProfile + .roles?.some( + role => EMAIL_VIEW_ROLES.some(allowed => role.toLowerCase() === allowed.toLowerCase()), + ) + ) { + return true + } + + return false +} + export function getAvailabilityLabel(value?: string): string | undefined { return availabilityOptions.find(o => o.value === value)?.label } diff --git a/src/apps/profiles/src/member-profile/phones/Phones.tsx b/src/apps/profiles/src/member-profile/phones/Phones.tsx index f94cd186f..28cc09304 100644 --- a/src/apps/profiles/src/member-profile/phones/Phones.tsx +++ b/src/apps/profiles/src/member-profile/phones/Phones.tsx @@ -6,7 +6,7 @@ import { CopyButton } from '~/apps/admin/src/lib/components/CopyButton' import { IconOutline, IconSolid, Tooltip } from '~/libs/ui' import { AddButton } from '../../components' -import { canSeePhones } from '../../lib/helpers' +import { canSeeEmail, canSeePhones } from '../../lib/helpers' import { ModifyPhonesModal } from './ModifyPhonesModal' import { PhoneCard } from './PhoneCard' @@ -21,7 +21,7 @@ interface PhonesProps { const Phones: FC = (props: PhonesProps) => { const canEdit: boolean = props.authProfile?.handle === props.profile.handle const canSeePhonesValue: boolean = canSeePhones(props.authProfile, props.profile) - const canSeeEmail: boolean = props.authProfile?.userId === props.profile.userId + const canSeeEmailValue: boolean = canSeeEmail(props.authProfile, props.profile) const [isEditMode, setIsEditMode]: [boolean, Dispatch>] = useState(false) @@ -62,7 +62,7 @@ const Phones: FC = (props: PhonesProps) => { )}

- {canSeeEmail && props.profile?.email && ( + {canSeeEmailValue && props.profile?.email && (
From 95e3658c3154c54a8580e80a9fae34034040630d Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 12 Feb 2026 14:16:06 +0200 Subject: [PATCH 07/13] PM-3825 #time 1h - make sure we properly restrict visibility over user profile sensitive data --- .../src/member-profile/profile-header/ProfileHeader.tsx | 1 - src/libs/ui/lib/components/tooltip/Tooltip.module.scss | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx index a59aea165..2c1bc1cb0 100644 --- a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx +++ b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx @@ -46,7 +46,6 @@ const ProfileHeader: FC = (props: ProfileHeaderProps) => { = !canEdit && ( roles.includes(UserRole.administrator) - || roles.includes(UserRole.projectManager) || roles.includes(UserRole.talentManager) ) diff --git a/src/libs/ui/lib/components/tooltip/Tooltip.module.scss b/src/libs/ui/lib/components/tooltip/Tooltip.module.scss index 27413ddcc..87d471f5e 100644 --- a/src/libs/ui/lib/components/tooltip/Tooltip.module.scss +++ b/src/libs/ui/lib/components/tooltip/Tooltip.module.scss @@ -10,7 +10,7 @@ background-color: $tc-black; color: $black-5; border-radius: $sp-2; - opacity: 1 !important; + opacity: 1; box-shadow: 0 1px 5px $tips-shadow; text-transform: none; max-width: 32rem; From eeccab3af77efeed99de1eb85cb8335a54d467b8 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 13 Feb 2026 09:28:46 +0200 Subject: [PATCH 08/13] PM-3825 #time 30min profile privac --- src/apps/profiles/src/lib/helpers.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index 627d4a29c..63be38f28 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -145,17 +145,12 @@ export function canDownloadProfile(authProfile: UserProfile | undefined, profile return true } - // Check if user has admin roles - if (authProfile.roles?.some(role => ADMIN_ROLES.includes(role.toLowerCase() as UserRole))) { - return true - } - - // Check if user has PM or Talent Manager roles - const allowedRoles = ['Project Manager', 'Talent Manager'] - if (authProfile - .roles?.some( - role => allowedRoles.some(allowed => role.toLowerCase() === allowed.toLowerCase()), - ) + // Check if user has admin roles or talent manager + if ( + authProfile.roles?.some(role => [ + UserRole.talentManager, + ...ADMIN_ROLES, + ].includes(role.toLowerCase() as UserRole)) ) { return true } From ef88f60ce8b5bff59be9ec8d4817709bee769507 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 13 Feb 2026 11:15:23 +0200 Subject: [PATCH 09/13] PM-3825 #15m compare lowercase --- src/apps/profiles/src/lib/helpers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index 63be38f28..a09cab00d 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -150,7 +150,8 @@ export function canDownloadProfile(authProfile: UserProfile | undefined, profile authProfile.roles?.some(role => [ UserRole.talentManager, ...ADMIN_ROLES, - ].includes(role.toLowerCase() as UserRole)) + ].map(r => r.toLowerCase()) + .includes(role.toLowerCase() as UserRole)) ) { return true } From e20e595502db1a858cadb762b2d81311b3ac9339 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 13 Feb 2026 11:23:08 +0200 Subject: [PATCH 10/13] hide contact section for copilot & pm --- src/apps/profiles/src/member-profile/phones/Phones.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apps/profiles/src/member-profile/phones/Phones.tsx b/src/apps/profiles/src/member-profile/phones/Phones.tsx index 28cc09304..4ff4118d5 100644 --- a/src/apps/profiles/src/member-profile/phones/Phones.tsx +++ b/src/apps/profiles/src/member-profile/phones/Phones.tsx @@ -51,6 +51,10 @@ const Phones: FC = (props: PhonesProps) => { }, 1000) } + if (!canEdit && (!canSeeEmailValue || !(props.profile?.email || phones.length))) { + return <>; + } + return (
From df4060e969cef4c8ac171a574469f38cdf78d1e2 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 13 Feb 2026 11:24:18 +0200 Subject: [PATCH 11/13] hide phone & email for copilot & PM --- src/apps/profiles/src/config/constants.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/apps/profiles/src/config/constants.ts b/src/apps/profiles/src/config/constants.ts index 6d975e532..b53971427 100644 --- a/src/apps/profiles/src/config/constants.ts +++ b/src/apps/profiles/src/config/constants.ts @@ -34,12 +34,9 @@ export const ADMIN_ROLES = [UserRole.administrator] export const PHONE_NUMBER_ROLES = [ UserRole.administrator, UserRole.talentManager, - UserRole.projectManager, - UserRole.copilot, ] export const EMAIL_VIEW_ROLES = [ UserRole.administrator, UserRole.talentManager, - UserRole.projectManager, ] From 7743fcf7807d5b0cb8d5fbd9f9a18db90bf296de Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 13 Feb 2026 11:26:55 +0200 Subject: [PATCH 12/13] improve readability --- src/apps/profiles/src/member-profile/phones/Phones.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/profiles/src/member-profile/phones/Phones.tsx b/src/apps/profiles/src/member-profile/phones/Phones.tsx index 4ff4118d5..fb4059cf0 100644 --- a/src/apps/profiles/src/member-profile/phones/Phones.tsx +++ b/src/apps/profiles/src/member-profile/phones/Phones.tsx @@ -50,9 +50,10 @@ const Phones: FC = (props: PhonesProps) => { props.refreshProfile(props.profile.handle) }, 1000) } - - if (!canEdit && (!canSeeEmailValue || !(props.profile?.email || phones.length))) { - return <>; + // Don't render anything if user cannot edit AND cannot see any contact info + const hasContactInfo = props.profile?.email || phones.length > 0 + if ( !canEdit && (!canSeeEmailValue || !hasContactInfo)) { + return <> } return ( From aa72957b1fbd96fd3dbe837aca308299c7596423 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 13 Feb 2026 11:28:36 +0200 Subject: [PATCH 13/13] lint --- src/apps/profiles/src/member-profile/phones/Phones.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/profiles/src/member-profile/phones/Phones.tsx b/src/apps/profiles/src/member-profile/phones/Phones.tsx index fb4059cf0..126f495d0 100644 --- a/src/apps/profiles/src/member-profile/phones/Phones.tsx +++ b/src/apps/profiles/src/member-profile/phones/Phones.tsx @@ -50,9 +50,10 @@ const Phones: FC = (props: PhonesProps) => { props.refreshProfile(props.profile.handle) }, 1000) } + // Don't render anything if user cannot edit AND cannot see any contact info const hasContactInfo = props.profile?.email || phones.length > 0 - if ( !canEdit && (!canSeeEmailValue || !hasContactInfo)) { + if (!canEdit && (!canSeeEmailValue || !hasContactInfo)) { return <> }