Skip to content

Commit 04defd8

Browse files
committed
break render functions into standalone components [URO-208]
1 parent decebf5 commit 04defd8

18 files changed

+277
-223
lines changed

src/features/orcidlink/HomeLinked/LinkInfo.tsx

+15-8
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import {
22
LinkRecordPublic,
33
ORCIDProfile,
44
} from '../../../common/api/orcidLinkCommon';
5-
import {
6-
renderCreditName,
7-
renderORCIDId,
8-
renderRealname,
9-
} from '../common/misc';
5+
import CreditName from '../common/CreditName';
6+
import { ORCIDIdLink } from '../common/ORCIDIdLink';
7+
import RealName from '../common/RealName';
108
import Scopes from '../common/Scopes';
119
import styles from '../orcidlink.module.scss';
1210

@@ -26,15 +24,24 @@ export default function LinkInfo({
2624
<div className={styles['prop-table']}>
2725
<div>
2826
<div>ORCID iD</div>
29-
<div>{renderORCIDId(orcidSiteURL, linkRecord.orcid_auth.orcid)}</div>
27+
<div>
28+
<ORCIDIdLink
29+
url={orcidSiteURL}
30+
orcidId={linkRecord.orcid_auth.orcid}
31+
/>
32+
</div>
3033
</div>
3134
<div>
3235
<div>Name on Account</div>
33-
<div>{renderRealname(profile)}</div>
36+
<div>
37+
<RealName profile={profile} />
38+
</div>
3439
</div>
3540
<div>
3641
<div>Published Name</div>
37-
<div>{renderCreditName(profile)}</div>
42+
<div>
43+
<CreditName profile={profile} />
44+
</div>
3845
</div>
3946
<div>
4047
<div>Created on</div>

src/features/orcidlink/HomeLinked/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { InfoResult, orcidlinkAPI } from '../../../common/api/orcidlinkAPI';
22
import { useAppSelector } from '../../../common/hooks';
33
import { authUsername } from '../../auth/authSlice';
44
import ErrorMessage from '../common/ErrorMessage';
5-
import { renderLoadingOverlay } from '../common/misc';
5+
import LoadingOverlay from '../common/LoadingOverlay';
66
import HomeLinked from './view';
77

88
export interface HomeLinkedControllerProps {
@@ -35,7 +35,7 @@ export default function HomeLinkedController({
3535

3636
return (
3737
<>
38-
{renderLoadingOverlay(isFetching)}
38+
<LoadingOverlay open={isFetching} />
3939
{renderState()}
4040
</>
4141
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { render } from '@testing-library/react';
2+
import 'core-js/stable/structured-clone';
3+
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
4+
import { PROFILE_1 } from '../test/data';
5+
import CreditName from './CreditName';
6+
7+
describe('The renderCreditName render function', () => {
8+
it('renders correctly if not private', () => {
9+
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
10+
11+
const { container } = render(<CreditName profile={profile} />);
12+
13+
expect(container).toHaveTextContent('Foo B. Bar');
14+
});
15+
16+
it('renders a special string if it is private', () => {
17+
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
18+
19+
profile.nameGroup.private = true;
20+
21+
const { container } = render(<CreditName profile={profile} />);
22+
23+
expect(container).toHaveTextContent('private');
24+
});
25+
26+
it('renders a "not available" string if it is absent', () => {
27+
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
28+
29+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30+
profile.nameGroup.fields!.creditName = null;
31+
32+
const { container } = render(<CreditName profile={profile} />);
33+
34+
expect(container).toHaveTextContent('n/a');
35+
});
36+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Typography } from '@mui/material';
2+
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
3+
import NA from './NA';
4+
import PrivateField from './PrivateField';
5+
6+
export interface CreditNameProps {
7+
profile: ORCIDProfile;
8+
}
9+
10+
/**
11+
* Displays an ORCID user profile "published name" - an optional name a user may
12+
* specify in their ORCID profile to be used in publication credit, instead of
13+
* their given (required) and family names (optiona).
14+
*/
15+
export default function CreditName({ profile }: CreditNameProps) {
16+
if (profile.nameGroup.private) {
17+
return <PrivateField />;
18+
}
19+
if (!profile.nameGroup.fields.creditName) {
20+
return <NA />;
21+
}
22+
return <Typography>{profile.nameGroup.fields.creditName}</Typography>;
23+
}

src/features/orcidlink/common/ErrorMessage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* Displays an error message as may be retured by an RTK query.
33
*
4-
* Currently very basci, just displaying the message in an Alert. However, some
5-
* errors can clearly use a more specialized display.
4+
* Currently very basic, just displaying the message in an Alert. However, some
5+
* errors would benefit from a more specialized display.
66
*/
77
import Alert from '@mui/material/Alert';
88
import { SerializedError } from '@reduxjs/toolkit';
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
.orcid-icon {
2-
height: 24px;
3-
margin-right: 0.25em;
4-
}
51

62
/* A wrapper around a loading indicator when presented standalone */
73
.loading {
@@ -11,7 +7,7 @@
117
justify-content: center;
128
margin-top: 8rem;
139
}
14-
10+
1511
.loading-title {
1612
margin-left: 0.5rem;
1713
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Alert, AlertTitle, CircularProgress, Modal } from '@mui/material';
2+
import styles from './LoadingOverlay.module.scss';
3+
4+
export interface LoadingAlertProps {
5+
title: string;
6+
description: string;
7+
}
8+
9+
/**
10+
* A wrapper around MUI Alert to show a loading indicator (spinner) and message,
11+
* with a description.
12+
*/
13+
export function LoadingAlert({ title, description }: LoadingAlertProps) {
14+
return (
15+
<div className={styles.loading}>
16+
<Alert icon={<CircularProgress size="1rem" />}>
17+
<AlertTitle>
18+
<span className={styles['loading-title']}>{title}</span>
19+
</AlertTitle>
20+
<p>{description}</p>
21+
</Alert>
22+
</div>
23+
);
24+
}
25+
26+
export interface LoadingOverlayProps {
27+
open: boolean;
28+
}
29+
30+
/**
31+
* Displays a model containing a loading alert as defined above, for usage in
32+
* covering and blocking the screen while a process is in progress.
33+
*/
34+
export default function LoadingOverlay({ open }: LoadingOverlayProps) {
35+
return (
36+
<Modal open={open} disableAutoFocus={true}>
37+
<LoadingAlert title="Loading..." description="Loading ORCID Link" />
38+
</Modal>
39+
);
40+
}

src/features/orcidlink/common/NA.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Typography } from '@mui/material';
2+
3+
/**
4+
* Simply a common component to use in place of an empty space for a field which
5+
* is absent or empty.
6+
*/
7+
export default function NA() {
8+
return (
9+
<Typography fontStyle="italic" variant="body1">
10+
n/a
11+
</Typography>
12+
);
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.main {
2+
align-items: center;
3+
display: flex;
4+
flex-direction: row;
5+
}
6+
7+
.icon {
8+
height: 24px;
9+
margin-right: 0.25em;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { render, screen } from '@testing-library/react';
2+
import 'core-js/stable/structured-clone';
3+
import { ORCIDIdLink } from './ORCIDIdLink';
4+
5+
describe('The renderORCIDId render function', () => {
6+
it('renders an orcid id link', async () => {
7+
const baseURL = 'http://example.com';
8+
const orcidId = 'abc123';
9+
const expectedURL = `${baseURL}/${orcidId}`;
10+
11+
const { container } = render(
12+
<ORCIDIdLink url={baseURL} orcidId={orcidId} />
13+
);
14+
15+
expect(container).toHaveTextContent(orcidId);
16+
17+
const link = await screen.findByText(expectedURL);
18+
expect(link).toHaveAttribute('href', expectedURL);
19+
20+
const image = await screen.findByAltText('ORCID Icon');
21+
expect(image).toBeVisible();
22+
expect(image.getAttribute('src')).toContain('ORCID-iD_icon-vector.svg');
23+
});
24+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { image } from '../images';
2+
import styles from './ORCIDIdLink.module.scss';
3+
4+
export interface ORCIDIdLinkProps {
5+
url: string;
6+
orcidId: string;
7+
}
8+
9+
/**
10+
* Renders an anchor link to an ORCID profile in the form recommended by ORCID.
11+
*
12+
*/
13+
export function ORCIDIdLink({ url, orcidId }: ORCIDIdLinkProps) {
14+
return (
15+
<a
16+
href={`${url}/${orcidId}`}
17+
target="_blank"
18+
rel="noreferrer"
19+
className={styles.main}
20+
>
21+
<img src={image('orcidIcon')} alt="ORCID Icon" className={styles.icon} />
22+
{url}/{orcidId}
23+
</a>
24+
);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { render } from '@testing-library/react';
2+
import PrivateField from './PrivateField';
3+
4+
describe('The PrivateField component', () => {
5+
it('renders the expected text', () => {
6+
const { container } = render(<PrivateField />);
7+
expect(container).toHaveTextContent('private');
8+
});
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Typography } from '@mui/material';
2+
3+
/**
4+
* Should be used to indicate that an ORCID profile field has been made private
5+
* by the owner, and may not be viewed by anyone else.
6+
*
7+
*/
8+
export default function PrivateField() {
9+
return <Typography fontStyle="italic">private</Typography>;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { render } from '@testing-library/react';
2+
import 'core-js/stable/structured-clone';
3+
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
4+
import { PROFILE_1 } from '../test/data';
5+
import RealName from './RealName';
6+
7+
describe('the renderRealName render function ', () => {
8+
it('renders correctly if not private', () => {
9+
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
10+
11+
const { container } = render(<RealName profile={profile} />);
12+
13+
expect(container).toHaveTextContent('Foo Bar');
14+
});
15+
16+
it('renders just the first name if no last name', () => {
17+
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
18+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19+
profile.nameGroup.fields!.lastName = null;
20+
21+
const { container } = render(<RealName profile={profile} />);
22+
23+
expect(container).toHaveTextContent('Foo');
24+
});
25+
26+
it('renders a special string if it is private', () => {
27+
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
28+
profile.nameGroup.private = true;
29+
30+
const { container } = render(<RealName profile={profile} />);
31+
32+
expect(container).toHaveTextContent('private');
33+
});
34+
});
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Typography } from '@mui/material';
2+
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
3+
import PrivateField from './PrivateField';
4+
5+
export interface RealNameProps {
6+
profile: ORCIDProfile;
7+
}
8+
9+
/**
10+
* Renders user's name from their ORCID profile.
11+
*/
12+
export default function RealName({ profile }: RealNameProps) {
13+
if (profile.nameGroup.private) {
14+
return <PrivateField />;
15+
}
16+
17+
const { firstName, lastName } = profile.nameGroup.fields;
18+
if (lastName) {
19+
return (
20+
<Typography>
21+
{firstName} {lastName}
22+
</Typography>
23+
);
24+
}
25+
return <Typography>{firstName}</Typography>;
26+
}

src/features/orcidlink/common/Scopes.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ function isScope(possibleScope: string): possibleScope is ORCIDScope {
3030
return ['/read-limited', '/activities/update'].includes(possibleScope);
3131
}
3232

33+
/**
34+
* Renders the scopes as provided by an ORCID profile - a string containing
35+
* space-separated scope identifiers.
36+
*
37+
* The scopes are displayed in a collapsed "accordion" component, the detail
38+
* area of which contains the description of the associated scope.
39+
*/
3340
export default function Scopes({ scopes }: ScopesProps) {
3441
const rows = scopes.split(/\s+/).map((scope: string, index) => {
3542
if (!isScope(scope)) {

0 commit comments

Comments
 (0)