Skip to content

Commit

Permalink
break render functions into standalone components [URO-208]
Browse files Browse the repository at this point in the history
  • Loading branch information
eapearson committed May 20, 2024
1 parent decebf5 commit 04defd8
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 223 deletions.
23 changes: 15 additions & 8 deletions src/features/orcidlink/HomeLinked/LinkInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import {
LinkRecordPublic,
ORCIDProfile,
} from '../../../common/api/orcidLinkCommon';
import {
renderCreditName,
renderORCIDId,
renderRealname,
} from '../common/misc';
import CreditName from '../common/CreditName';
import { ORCIDIdLink } from '../common/ORCIDIdLink';
import RealName from '../common/RealName';
import Scopes from '../common/Scopes';
import styles from '../orcidlink.module.scss';

Expand All @@ -26,15 +24,24 @@ export default function LinkInfo({
<div className={styles['prop-table']}>
<div>
<div>ORCID iD</div>
<div>{renderORCIDId(orcidSiteURL, linkRecord.orcid_auth.orcid)}</div>
<div>
<ORCIDIdLink
url={orcidSiteURL}
orcidId={linkRecord.orcid_auth.orcid}
/>
</div>
</div>
<div>
<div>Name on Account</div>
<div>{renderRealname(profile)}</div>
<div>
<RealName profile={profile} />
</div>
</div>
<div>
<div>Published Name</div>
<div>{renderCreditName(profile)}</div>
<div>
<CreditName profile={profile} />
</div>
</div>
<div>
<div>Created on</div>
Expand Down
4 changes: 2 additions & 2 deletions src/features/orcidlink/HomeLinked/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { InfoResult, orcidlinkAPI } from '../../../common/api/orcidlinkAPI';
import { useAppSelector } from '../../../common/hooks';
import { authUsername } from '../../auth/authSlice';
import ErrorMessage from '../common/ErrorMessage';
import { renderLoadingOverlay } from '../common/misc';
import LoadingOverlay from '../common/LoadingOverlay';
import HomeLinked from './view';

export interface HomeLinkedControllerProps {
Expand Down Expand Up @@ -35,7 +35,7 @@ export default function HomeLinkedController({

return (
<>
{renderLoadingOverlay(isFetching)}
<LoadingOverlay open={isFetching} />
{renderState()}
</>
);
Expand Down
36 changes: 36 additions & 0 deletions src/features/orcidlink/common/CreditName.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render } from '@testing-library/react';
import 'core-js/stable/structured-clone';
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
import { PROFILE_1 } from '../test/data';
import CreditName from './CreditName';

describe('The renderCreditName render function', () => {
it('renders correctly if not private', () => {
const profile = structuredClone<ORCIDProfile>(PROFILE_1);

const { container } = render(<CreditName profile={profile} />);

expect(container).toHaveTextContent('Foo B. Bar');
});

it('renders a special string if it is private', () => {
const profile = structuredClone<ORCIDProfile>(PROFILE_1);

profile.nameGroup.private = true;

const { container } = render(<CreditName profile={profile} />);

expect(container).toHaveTextContent('private');
});

it('renders a "not available" string if it is absent', () => {
const profile = structuredClone<ORCIDProfile>(PROFILE_1);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
profile.nameGroup.fields!.creditName = null;

const { container } = render(<CreditName profile={profile} />);

expect(container).toHaveTextContent('n/a');
});
});
23 changes: 23 additions & 0 deletions src/features/orcidlink/common/CreditName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Typography } from '@mui/material';
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
import NA from './NA';
import PrivateField from './PrivateField';

export interface CreditNameProps {
profile: ORCIDProfile;
}

/**
* Displays an ORCID user profile "published name" - an optional name a user may
* specify in their ORCID profile to be used in publication credit, instead of
* their given (required) and family names (optiona).
*/
export default function CreditName({ profile }: CreditNameProps) {
if (profile.nameGroup.private) {
return <PrivateField />;
}
if (!profile.nameGroup.fields.creditName) {
return <NA />;
}
return <Typography>{profile.nameGroup.fields.creditName}</Typography>;
}
4 changes: 2 additions & 2 deletions src/features/orcidlink/common/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Displays an error message as may be retured by an RTK query.
*
* Currently very basci, just displaying the message in an Alert. However, some
* errors can clearly use a more specialized display.
* Currently very basic, just displaying the message in an Alert. However, some
* errors would benefit from a more specialized display.
*/
import Alert from '@mui/material/Alert';
import { SerializedError } from '@reduxjs/toolkit';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.orcid-icon {
height: 24px;
margin-right: 0.25em;
}

/* A wrapper around a loading indicator when presented standalone */
.loading {
Expand All @@ -11,7 +7,7 @@
justify-content: center;
margin-top: 8rem;
}

.loading-title {
margin-left: 0.5rem;
}
40 changes: 40 additions & 0 deletions src/features/orcidlink/common/LoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Alert, AlertTitle, CircularProgress, Modal } from '@mui/material';
import styles from './LoadingOverlay.module.scss';

export interface LoadingAlertProps {
title: string;
description: string;
}

/**
* A wrapper around MUI Alert to show a loading indicator (spinner) and message,
* with a description.
*/
export function LoadingAlert({ title, description }: LoadingAlertProps) {
return (
<div className={styles.loading}>
<Alert icon={<CircularProgress size="1rem" />}>
<AlertTitle>
<span className={styles['loading-title']}>{title}</span>
</AlertTitle>
<p>{description}</p>
</Alert>
</div>
);
}

export interface LoadingOverlayProps {
open: boolean;
}

/**
* Displays a model containing a loading alert as defined above, for usage in
* covering and blocking the screen while a process is in progress.
*/
export default function LoadingOverlay({ open }: LoadingOverlayProps) {
return (
<Modal open={open} disableAutoFocus={true}>
<LoadingAlert title="Loading..." description="Loading ORCID Link" />
</Modal>
);
}
13 changes: 13 additions & 0 deletions src/features/orcidlink/common/NA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Typography } from '@mui/material';

/**
* Simply a common component to use in place of an empty space for a field which
* is absent or empty.
*/
export default function NA() {
return (
<Typography fontStyle="italic" variant="body1">
n/a
</Typography>
);
}
10 changes: 10 additions & 0 deletions src/features/orcidlink/common/ORCIDIdLink.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.main {
align-items: center;
display: flex;
flex-direction: row;
}

.icon {
height: 24px;
margin-right: 0.25em;
}
24 changes: 24 additions & 0 deletions src/features/orcidlink/common/ORCIDIdLink.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react';
import 'core-js/stable/structured-clone';
import { ORCIDIdLink } from './ORCIDIdLink';

describe('The renderORCIDId render function', () => {
it('renders an orcid id link', async () => {
const baseURL = 'http://example.com';
const orcidId = 'abc123';
const expectedURL = `${baseURL}/${orcidId}`;

const { container } = render(
<ORCIDIdLink url={baseURL} orcidId={orcidId} />
);

expect(container).toHaveTextContent(orcidId);

const link = await screen.findByText(expectedURL);
expect(link).toHaveAttribute('href', expectedURL);

const image = await screen.findByAltText('ORCID Icon');
expect(image).toBeVisible();
expect(image.getAttribute('src')).toContain('ORCID-iD_icon-vector.svg');
});
});
25 changes: 25 additions & 0 deletions src/features/orcidlink/common/ORCIDIdLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { image } from '../images';
import styles from './ORCIDIdLink.module.scss';

export interface ORCIDIdLinkProps {
url: string;
orcidId: string;
}

/**
* Renders an anchor link to an ORCID profile in the form recommended by ORCID.
*
*/
export function ORCIDIdLink({ url, orcidId }: ORCIDIdLinkProps) {
return (
<a
href={`${url}/${orcidId}`}
target="_blank"
rel="noreferrer"
className={styles.main}
>
<img src={image('orcidIcon')} alt="ORCID Icon" className={styles.icon} />
{url}/{orcidId}
</a>
);
}
9 changes: 9 additions & 0 deletions src/features/orcidlink/common/PrivateField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { render } from '@testing-library/react';
import PrivateField from './PrivateField';

describe('The PrivateField component', () => {
it('renders the expected text', () => {
const { container } = render(<PrivateField />);
expect(container).toHaveTextContent('private');
});
});
10 changes: 10 additions & 0 deletions src/features/orcidlink/common/PrivateField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Typography } from '@mui/material';

/**
* Should be used to indicate that an ORCID profile field has been made private
* by the owner, and may not be viewed by anyone else.
*
*/
export default function PrivateField() {
return <Typography fontStyle="italic">private</Typography>;
}
34 changes: 34 additions & 0 deletions src/features/orcidlink/common/RealName.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render } from '@testing-library/react';
import 'core-js/stable/structured-clone';
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
import { PROFILE_1 } from '../test/data';
import RealName from './RealName';

describe('the renderRealName render function ', () => {
it('renders correctly if not private', () => {
const profile = structuredClone<ORCIDProfile>(PROFILE_1);

const { container } = render(<RealName profile={profile} />);

expect(container).toHaveTextContent('Foo Bar');
});

it('renders just the first name if no last name', () => {
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
profile.nameGroup.fields!.lastName = null;

const { container } = render(<RealName profile={profile} />);

expect(container).toHaveTextContent('Foo');
});

it('renders a special string if it is private', () => {
const profile = structuredClone<ORCIDProfile>(PROFILE_1);
profile.nameGroup.private = true;

const { container } = render(<RealName profile={profile} />);

expect(container).toHaveTextContent('private');
});
});
26 changes: 26 additions & 0 deletions src/features/orcidlink/common/RealName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Typography } from '@mui/material';
import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
import PrivateField from './PrivateField';

export interface RealNameProps {
profile: ORCIDProfile;
}

/**
* Renders user's name from their ORCID profile.
*/
export default function RealName({ profile }: RealNameProps) {
if (profile.nameGroup.private) {
return <PrivateField />;
}

const { firstName, lastName } = profile.nameGroup.fields;
if (lastName) {
return (
<Typography>
{firstName} {lastName}
</Typography>
);
}
return <Typography>{firstName}</Typography>;
}
7 changes: 7 additions & 0 deletions src/features/orcidlink/common/Scopes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ function isScope(possibleScope: string): possibleScope is ORCIDScope {
return ['/read-limited', '/activities/update'].includes(possibleScope);
}

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

0 comments on commit 04defd8

Please sign in to comment.