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 @@ -16,4 +16,9 @@
.message {
font-size: 14px;
margin: 0;

a {
color: inherit;
text-decoration: underline;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable import/no-extraneous-dependencies */
import {
render,
screen,
} from '@testing-library/react'

import { ErrorMessage } from './ErrorMessage'

jest.mock('~/libs/ui', () => ({
Button: (props: { label: string }) => (
<button type='button'>{props.label}</button>
),
}), {
virtual: true,
})

describe('ErrorMessage', () => {
it('renders the Topcoder support email as a mailto link', () => {
const message = 'You don’t have access to this project. Please contact support@topcoder.com.'

render(<ErrorMessage message={message} />)

const supportLink = screen.getByRole('link', { name: 'support@topcoder.com' })

expect(supportLink.getAttribute('href'))
.toBe('mailto:support@topcoder.com')
expect(supportLink.closest('p')?.textContent)
.toBe(message)
})
})
37 changes: 34 additions & 3 deletions src/apps/work/src/lib/components/ErrorMessage/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
import { FC } from 'react'
import {
FC,
ReactNode,
} from 'react'

import { Button } from '~/libs/ui'

import styles from './ErrorMessage.module.scss'

interface ErrorMessageProps {
message: string
message: ReactNode
onRetry?: () => void
}

const SUPPORT_EMAIL = 'support@topcoder.com'

/**
* Renders the supplied error message with the Topcoder support email converted to a mailto link.
*
* @param message error message text or custom React content to display.
* @returns message content with the support email linked when the message is plain text.
* @remarks Used by ErrorMessage so project access denial messages can keep their configured copy while linking support.
* @throws Does not throw.
*/
function renderMessage(message: ReactNode): ReactNode {
if (typeof message !== 'string' || !message.includes(SUPPORT_EMAIL)) {
return message
}

const emailIndex = message.indexOf(SUPPORT_EMAIL)

return (
<>
{message.slice(0, emailIndex)}
<a href={`mailto:${SUPPORT_EMAIL}`}>
{SUPPORT_EMAIL}
</a>
{message.slice(emailIndex + SUPPORT_EMAIL.length)}
</>
)
}

export const ErrorMessage: FC<ErrorMessageProps> = (props: ErrorMessageProps) => (
<div className={styles.container}>
<p className={styles.message}>{props.message}</p>
<p className={styles.message}>{renderMessage(props.message)}</p>
{props.onRetry
? (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,12 @@ describe('ProjectRouteAccessGuard', () => {

expect(screen.getByRole('heading', { level: 1, name: 'Users' }))
.toBeTruthy()
expect(screen.getByText(PROJECT_ACCESS_DENIED_MESSAGE))
.toBeTruthy()
const supportLink = screen.getByRole('link', { name: 'support@topcoder.com' })

expect(supportLink.getAttribute('href'))
.toBe('mailto:support@topcoder.com')
expect(supportLink.closest('p')?.textContent)
.toBe(PROJECT_ACCESS_DENIED_MESSAGE)
expect(screen.queryByText('Protected Project Users'))
.toBeNull()
})
Expand All @@ -211,8 +215,12 @@ describe('ProjectRouteAccessGuard', () => {

expect(mockedCheckProjectAccess)
.toHaveBeenCalledWith(defaultContextValue.userRoles, 12345, undefined)
expect(screen.getByText(PROJECT_ACCESS_DENIED_MESSAGE))
.toBeTruthy()
const supportLink = screen.getByRole('link', { name: 'support@topcoder.com' })

expect(supportLink.getAttribute('href'))
.toBe('mailto:support@topcoder.com')
expect(supportLink.closest('p')?.textContent)
.toBe(PROJECT_ACCESS_DENIED_MESSAGE)
expect(screen.queryByText('Protected Project Users'))
.toBeNull()
})
Expand Down
Loading