diff --git a/src/apps/profiles/src/config/constants.ts b/src/apps/profiles/src/config/constants.ts index 9122a1dcd..b53971427 100644 --- a/src/apps/profiles/src/config/constants.ts +++ b/src/apps/profiles/src/config/constants.ts @@ -34,6 +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, ] diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index f6e3e719c..a09cab00d 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 } @@ -145,17 +145,13 @@ 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, + ].map(r => r.toLowerCase()) + .includes(role.toLowerCase() as UserRole)) ) { return true } @@ -193,6 +189,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 3c87d2f33..126f495d0 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,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 canSeeEmailValue: boolean = canSeeEmail(props.authProfile, props.profile) const [isEditMode, setIsEditMode]: [boolean, Dispatch>] = useState(false) @@ -50,6 +51,12 @@ const Phones: FC = (props: PhonesProps) => { }, 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)) { + return <> + } + return (
@@ -61,7 +68,7 @@ const Phones: FC = (props: PhonesProps) => { )}

- {props.profile?.email && ( + {canSeeEmailValue && props.profile?.email && (
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/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index c23698107..fee7d626b 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -45,6 +45,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' @@ -240,24 +241,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, @@ -288,13 +278,21 @@ export const TabContentSubmissions: FC = props => { ? undefined : submission.virusScan const failedScan = normalizedVirusScan === false - const isRestricted = isRestrictedBase || failedScan - const tooltipMessage = failedScan + const cannotDownloadSubmission = ( + !canViewSubmissions && String(submission.memberId) === String(loginUserInfo?.userId) + ) + const isRestricted = isRestrictedBase || failedScan || !cannotDownloadSubmission + let tooltipMessage = failedScan ? VIRUS_SCAN_FAILED_MESSAGE : ( getRestrictionMessageForMember(submission.memberId) ?? restrictionMessage ) + + if (!cannotDownloadSubmission) { + 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 6b18218eb..eea1146e6 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, @@ -396,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 9f37c1c73..fd161e7dc 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], ) const scorecardIds = useMemo>(() => { @@ -373,6 +368,7 @@ export const TableReviewForSubmitter: FC = (props: isDownloading, isSubmissionDownloadRestricted, 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.' diff --git a/src/libs/shared/lib/components/skill-pill/SkillPill.tsx b/src/libs/shared/lib/components/skill-pill/SkillPill.tsx index ebb021343..9cbb0f5c0 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_2nd_place: 'Win', + challenge_3rd_place: 'Win', + challenge_copilot: 'Copilot', + challenge_finisher: 'Submit', + challenge_review: 'Review', + challenge_win: 'Win', +} + 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} 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;