Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,23 +113,29 @@ function getDecisionBySubmission(
}

/**
* Builds a list of human-readable note strings from escalations and unlock reason.
* Builds a list of human-readable note strings from escalations.
* Used to display notes for Escalating, Approving/Rejecting, and Unlocking actions.
*/
function buildDecisionNotes(
escalations?: AiReviewDecisionEscalation[],
reason?: string | null,
resourceMemberIdMapping?: Record<string, any>,
): string[] {
const parts: string[] = []

const getMemberHandle = (memberId?: string | null): string => {
if (!memberId || !resourceMemberIdMapping) return ''
const resource = resourceMemberIdMapping[memberId]
return resource?.memberHandle || ''
}

escalations?.forEach(esc => {
if (esc.escalationNotes) {
const by = esc.createdBy ? ` (by ${esc.createdBy})` : ''
const by = esc.createdBy ? ` (by ${getMemberHandle(esc.createdBy)})` : ''
parts.push(`Escalation Note${by}: ${esc.escalationNotes}`)
}

if (esc.approverNotes) {
const by = esc.updatedBy ? ` (by ${esc.updatedBy})` : ''
const by = esc.updatedBy ? ` (by ${getMemberHandle(esc.updatedBy)})` : ''
const prefix = esc.status === 'APPROVED'
? 'Approval Note'
: esc.status === 'REJECTED'
Expand All @@ -139,10 +145,6 @@ function buildDecisionNotes(
}
})

if (reason) {
parts.push(`Unlock Reason: ${reason}`)
}

return parts
}

Expand Down Expand Up @@ -334,17 +336,18 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
return 'Submission Locked - This submission won\'t be reviewed during the Review Phase.'
}, [currentDecision?.submissionLocked, hasSubmitterRole])

const resourceMemberIdMapping = challengeDetailContext.resourceMemberIdMapping

/**
* Builds the list of notes from escalations (escalationNotes, approverNotes)
* and the unlock reason. These are shown to Copilot/Manager/Admin only
* (not to submitters) so they can see why a submission was escalated,
* approved/rejected, or unlocked.
* Builds the list of notes from escalations (escalationNotes, approverNotes).
* These are shown to Copilot/Manager/Admin only (not to submitters) so they
* can see why a submission was escalated or approved/rejected.
*/
const decisionNotes = useMemo((): string[] => {
if (!currentDecision || hasSubmitterRole) return []

return buildDecisionNotes(currentDecision.escalations, currentDecision.reason)
}, [currentDecision, hasSubmitterRole])
return buildDecisionNotes(currentDecision.escalations, resourceMemberIdMapping)
}, [currentDecision, hasSubmitterRole, resourceMemberIdMapping])

const hasDecisionNotes = decisionNotes.length > 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,31 @@ export function normalizeDecisionStatus(
}

/**
* Builds a multi-line tooltip string from escalation notes, approver notes,
* and the unlock reason. Returns undefined if there are no notes at all.
* Used to show Copilot/Manager/Admin why a submission was escalated,
* approved/rejected, or unlocked.
* Builds a multi-line tooltip string from escalation notes and approver notes.
* Returns undefined if there are no notes at all.
* Used to show Copilot/Manager/Admin why a submission was escalated or
* approved/rejected.
*/
function buildNotesTooltip(
escalations?: AiReviewDecisionEscalation[],
reason?: string | null,
resourceMemberIdMapping?: Record<string, any>,
): string | undefined {
const parts: string[] = []

const getMemberHandle = (memberId?: string | null): string => {
if (!memberId || !resourceMemberIdMapping) return ''
const resource = resourceMemberIdMapping[memberId]
return resource?.memberHandle || ''
}

escalations?.forEach(esc => {
if (esc.escalationNotes) {
const by = esc.createdBy ? ` (by ${esc.createdBy})` : ''
const by = esc.createdBy ? ` (by ${getMemberHandle(esc.createdBy)})` : ''
parts.push(`Escalation Note${by}: ${esc.escalationNotes}`)
}

if (esc.approverNotes) {
const by = esc.updatedBy ? ` (by ${esc.updatedBy})` : ''
const by = esc.updatedBy ? ` (by ${getMemberHandle(esc.updatedBy)})` : ''
const prefix = esc.status === 'APPROVED'
? 'Approval Note'
: esc.status === 'REJECTED'
Expand All @@ -81,10 +87,6 @@ function buildNotesTooltip(
}
})

if (reason) {
parts.push(`Unlock Reason: ${reason}`)
}

return parts.length ? parts.join('\n') : undefined
}

Expand All @@ -111,16 +113,18 @@ const CollapsibleAiReviewsRow: FC<CollapsibleAiReviewsRowProps> = props => {
[currentDecision?.status],
)

const resourceMemberIdMapping = challengeDetailContext.resourceMemberIdMapping

/**
* Builds the tooltip text for the notes icon shown next to the status label.
* Only shown to Copilot/Manager/Admin (not submitters).
* Covers: escalation notes, approval/rejection notes, and unlock reason.
* Covers: escalation notes and approval/rejection notes.
*/
const notesTooltip = useMemo((): string | undefined => {
if (hasSubmitterRole || !currentDecision) return undefined

return buildNotesTooltip(currentDecision.escalations, currentDecision.reason)
}, [currentDecision, hasSubmitterRole])
return buildNotesTooltip(currentDecision.escalations, resourceMemberIdMapping)
}, [currentDecision, hasSubmitterRole, resourceMemberIdMapping])

const [isOpen, setIsOpen] = useState(props.defaultOpen ?? false)
const [portalContainer, setPortalContainer] = useState<HTMLTableCellElement | undefined>(undefined)
Expand Down
Loading