Created on
diff --git a/src/features/orcidlink/HomeLinked/index.tsx b/src/features/orcidlink/HomeLinked/index.tsx
index de595c2b..95d34cac 100644
--- a/src/features/orcidlink/HomeLinked/index.tsx
+++ b/src/features/orcidlink/HomeLinked/index.tsx
@@ -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 {
@@ -35,7 +35,7 @@ export default function HomeLinkedController({
return (
<>
- {renderLoadingOverlay(isFetching)}
+
{renderState()}
>
);
diff --git a/src/features/orcidlink/common/CreditName.test.tsx b/src/features/orcidlink/common/CreditName.test.tsx
new file mode 100644
index 00000000..5f36f895
--- /dev/null
+++ b/src/features/orcidlink/common/CreditName.test.tsx
@@ -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
(PROFILE_1);
+
+ const { container } = render();
+
+ expect(container).toHaveTextContent('Foo B. Bar');
+ });
+
+ it('renders a special string if it is private', () => {
+ const profile = structuredClone(PROFILE_1);
+
+ profile.nameGroup.private = true;
+
+ const { container } = render();
+
+ expect(container).toHaveTextContent('private');
+ });
+
+ it('renders a "not available" string if it is absent', () => {
+ const profile = structuredClone(PROFILE_1);
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ profile.nameGroup.fields!.creditName = null;
+
+ const { container } = render();
+
+ expect(container).toHaveTextContent('n/a');
+ });
+});
diff --git a/src/features/orcidlink/common/CreditName.tsx b/src/features/orcidlink/common/CreditName.tsx
new file mode 100644
index 00000000..fbb420d6
--- /dev/null
+++ b/src/features/orcidlink/common/CreditName.tsx
@@ -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 ;
+ }
+ if (!profile.nameGroup.fields.creditName) {
+ return ;
+ }
+ return {profile.nameGroup.fields.creditName};
+}
diff --git a/src/features/orcidlink/common/ErrorMessage.tsx b/src/features/orcidlink/common/ErrorMessage.tsx
index f133457c..938958c7 100644
--- a/src/features/orcidlink/common/ErrorMessage.tsx
+++ b/src/features/orcidlink/common/ErrorMessage.tsx
@@ -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';
diff --git a/src/features/orcidlink/common/misc.module.scss b/src/features/orcidlink/common/LoadingOverlay.module.scss
similarity index 79%
rename from src/features/orcidlink/common/misc.module.scss
rename to src/features/orcidlink/common/LoadingOverlay.module.scss
index 69a5a2f6..6958b70b 100644
--- a/src/features/orcidlink/common/misc.module.scss
+++ b/src/features/orcidlink/common/LoadingOverlay.module.scss
@@ -1,7 +1,3 @@
-.orcid-icon {
- height: 24px;
- margin-right: 0.25em;
-}
/* A wrapper around a loading indicator when presented standalone */
.loading {
@@ -11,7 +7,7 @@
justify-content: center;
margin-top: 8rem;
}
-
+
.loading-title {
margin-left: 0.5rem;
}
diff --git a/src/features/orcidlink/common/LoadingOverlay.tsx b/src/features/orcidlink/common/LoadingOverlay.tsx
new file mode 100644
index 00000000..1b436c12
--- /dev/null
+++ b/src/features/orcidlink/common/LoadingOverlay.tsx
@@ -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 (
+
+
}>
+
+ {title}
+
+
{description}
+
+
+ );
+}
+
+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 (
+
+
+
+ );
+}
diff --git a/src/features/orcidlink/common/NA.tsx b/src/features/orcidlink/common/NA.tsx
new file mode 100644
index 00000000..3a2430fc
--- /dev/null
+++ b/src/features/orcidlink/common/NA.tsx
@@ -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 (
+
+ n/a
+
+ );
+}
diff --git a/src/features/orcidlink/common/ORCIDIdLink.module.scss b/src/features/orcidlink/common/ORCIDIdLink.module.scss
new file mode 100644
index 00000000..fd260763
--- /dev/null
+++ b/src/features/orcidlink/common/ORCIDIdLink.module.scss
@@ -0,0 +1,10 @@
+.main {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+}
+
+.icon {
+ height: 24px;
+ margin-right: 0.25em;
+}
diff --git a/src/features/orcidlink/common/ORCIDIdLink.test.tsx b/src/features/orcidlink/common/ORCIDIdLink.test.tsx
new file mode 100644
index 00000000..00eec477
--- /dev/null
+++ b/src/features/orcidlink/common/ORCIDIdLink.test.tsx
@@ -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(
+
+ );
+
+ 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');
+ });
+});
diff --git a/src/features/orcidlink/common/ORCIDIdLink.tsx b/src/features/orcidlink/common/ORCIDIdLink.tsx
new file mode 100644
index 00000000..59e9cfb0
--- /dev/null
+++ b/src/features/orcidlink/common/ORCIDIdLink.tsx
@@ -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 (
+
+
+ {url}/{orcidId}
+
+ );
+}
diff --git a/src/features/orcidlink/common/PrivateField.test.tsx b/src/features/orcidlink/common/PrivateField.test.tsx
new file mode 100644
index 00000000..365425c8
--- /dev/null
+++ b/src/features/orcidlink/common/PrivateField.test.tsx
@@ -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();
+ expect(container).toHaveTextContent('private');
+ });
+});
diff --git a/src/features/orcidlink/common/PrivateField.tsx b/src/features/orcidlink/common/PrivateField.tsx
new file mode 100644
index 00000000..9511bee9
--- /dev/null
+++ b/src/features/orcidlink/common/PrivateField.tsx
@@ -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 private;
+}
diff --git a/src/features/orcidlink/common/RealName.test.tsx b/src/features/orcidlink/common/RealName.test.tsx
new file mode 100644
index 00000000..c5d95fcc
--- /dev/null
+++ b/src/features/orcidlink/common/RealName.test.tsx
@@ -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(PROFILE_1);
+
+ const { container } = render();
+
+ expect(container).toHaveTextContent('Foo Bar');
+ });
+
+ it('renders just the first name if no last name', () => {
+ const profile = structuredClone(PROFILE_1);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ profile.nameGroup.fields!.lastName = null;
+
+ const { container } = render();
+
+ expect(container).toHaveTextContent('Foo');
+ });
+
+ it('renders a special string if it is private', () => {
+ const profile = structuredClone(PROFILE_1);
+ profile.nameGroup.private = true;
+
+ const { container } = render();
+
+ expect(container).toHaveTextContent('private');
+ });
+});
diff --git a/src/features/orcidlink/common/RealName.tsx b/src/features/orcidlink/common/RealName.tsx
new file mode 100644
index 00000000..cc376ffb
--- /dev/null
+++ b/src/features/orcidlink/common/RealName.tsx
@@ -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 ;
+ }
+
+ const { firstName, lastName } = profile.nameGroup.fields;
+ if (lastName) {
+ return (
+
+ {firstName} {lastName}
+
+ );
+ }
+ return {firstName};
+}
diff --git a/src/features/orcidlink/common/Scopes.tsx b/src/features/orcidlink/common/Scopes.tsx
index 41f401f9..2116b275 100644
--- a/src/features/orcidlink/common/Scopes.tsx
+++ b/src/features/orcidlink/common/Scopes.tsx
@@ -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)) {
diff --git a/src/features/orcidlink/common/misc.test.tsx b/src/features/orcidlink/common/misc.test.tsx
deleted file mode 100644
index f958edc4..00000000
--- a/src/features/orcidlink/common/misc.test.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import 'core-js/stable/structured-clone';
-import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
-import { PROFILE_1 } from '../test/data';
-import { renderCreditName, renderORCIDId, renderRealname } from './misc';
-
-describe('The miscellaneous shared components module', () => {
- describe('the renderRealName render function ', () => {
- it('renders correctly if not private', () => {
- const profile = structuredClone(PROFILE_1);
-
- const { container } = render(renderRealname(profile));
-
- expect(container).toHaveTextContent('Foo Bar');
- });
-
- it('renders just the first name if no last name', () => {
- const profile = structuredClone(PROFILE_1);
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- profile.nameGroup.fields!.lastName = null;
-
- const { container } = render(renderRealname(profile));
-
- expect(container).toHaveTextContent('Foo');
- });
-
- it('renders a special string if it is private', () => {
- const profile = structuredClone(PROFILE_1);
- profile.nameGroup.private = true;
-
- const { container } = render(renderRealname(profile));
-
- expect(container).toHaveTextContent('private');
- });
- });
-
- describe('The renderCreditName render function', () => {
- it('renders correctly if not private', () => {
- const profile = structuredClone(PROFILE_1);
-
- const { container } = render(renderCreditName(profile));
-
- expect(container).toHaveTextContent('Foo B. Bar');
- });
-
- it('renders a special string if it is private', () => {
- const profile = structuredClone(PROFILE_1);
-
- profile.nameGroup.private = true;
-
- const { container } = render(renderCreditName(profile));
-
- expect(container).toHaveTextContent('private');
- });
-
- it('renders a "not available" string if it is absent', () => {
- const profile = structuredClone(PROFILE_1);
-
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- profile.nameGroup.fields!.creditName = null;
-
- const { container } = render(renderCreditName(profile));
-
- expect(container).toHaveTextContent('n/a');
- });
- });
-
- 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(renderORCIDId(baseURL, 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');
- });
- });
-});
diff --git a/src/features/orcidlink/common/misc.tsx b/src/features/orcidlink/common/misc.tsx
deleted file mode 100644
index ec12615e..00000000
--- a/src/features/orcidlink/common/misc.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Miscellaneous commmonly used rendering functions
- *
- * If a bit of rendered content is used more than once, it should be wrapped in
- * a function and moved here.
- *
- */
-import {
- Alert,
- AlertTitle,
- CircularProgress,
- Modal,
- Typography,
-} from '@mui/material';
-import { ORCIDProfile } from '../../../common/api/orcidLinkCommon';
-import { image } from '../images';
-import styles from './misc.module.scss';
-
-export function renderORCIDIcon() {
- return (
-
- );
-}
-
-function renderPrivateField() {
- return private;
-}
-
-function renderNA() {
- return (
-
- n/a
-
- );
-}
-
-export function renderRealname(profile: ORCIDProfile) {
- // Name is the one stored from the original linking, may have changed.
- if (profile.nameGroup.private) {
- return renderPrivateField();
- }
-
- const { firstName, lastName } = profile.nameGroup.fields;
- if (lastName) {
- return (
-
- {firstName} {lastName}
-
- );
- }
- return {firstName};
-}
-
-/**
- * Renders the "credit name" from an ORCID User Profile.
- *
- * This is an optional field, so we are repared to render as n/a.
- *
- * @param profile An ORCID User Profile
- * @returns
- */
-export function renderCreditName(profile: ORCIDProfile) {
- if (profile.nameGroup.private) {
- return renderPrivateField();
- }
- if (!profile.nameGroup.fields.creditName) {
- return renderNA();
- }
- return {profile.nameGroup.fields.creditName};
-}
-
-function renderLoading(title: string, description: string) {
- return (
-
-
}>
-
- {title}
-
-
{description}
-
-
- );
-}
-
-export function renderLoadingOverlay(open: boolean) {
- return (
-
- {renderLoading('Loading...', 'Loading ORCID Link')}
-
- );
-}
-
-/**
- * Creates a link to an ORCID profile in the form recommended by ORCID.
- *
- * @param orcidSiteURL A URL to an ORCID public site that hosts orcid profiles
- * @param orcidId An ORCID iD
- * @returns An anchor component linking to the given ORCID iD
- */
-export function renderORCIDId(orcidSiteURL: string, orcidId: string) {
- return (
-
- {renderORCIDIcon()}
- {orcidSiteURL}/{orcidId}
-
- );
-}