Skip to content

Commit

Permalink
feat: author orc id link (#719)
Browse files Browse the repository at this point in the history
#525

- Implements the author link component with orc ID icon + link, dashed
link styling, and email icon
- Adds author link to dataset description and annotation side panel

## Demos

https://dev-orc-id.cryoet.dev.si.czi.technology/

<img width="1132" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/6a741bfb-3f8a-492c-9076-63448fcc9577">

<img width="447" alt="image"
src="https://github.com/chanzuckerberg/cryoet-data-portal/assets/2176050/b1bc0e95-1e72-45eb-bc7c-edbc136bacb3">
  • Loading branch information
codemonkey800 authored May 15, 2024
1 parent 28a0738 commit 9488f2a
Show file tree
Hide file tree
Showing 27 changed files with 588 additions and 159 deletions.
1 change: 1 addition & 0 deletions frontend/packages/data-portal/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
coverage/
node_modules/
/test-results/
/playwright-report/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ export function AuthorLegend({ inline = false }: { inline?: boolean }) {
<Tooltip
title={<Legend />}
classes={{
tooltip: '!p-sds-m !min-w-fit border-solid border border-sds-gray-300',
tooltip:
// need to specify background color because it's not visible locally
'!p-sds-m !min-w-fit border-solid border border-sds-gray-300 !bg-white',
}}
placement="top-start"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ComponentProps } from 'react'

import { MockLinkComponent } from 'app/components/Link'

import { AuthorLink } from './AuthorLink'

export function MockAuthorLink({
author,
large,
}: ComponentProps<typeof AuthorLink>) {
return (
<AuthorLink
author={author}
large={large}
LinkComponent={MockLinkComponent}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { it } from '@jest/globals'
import { render, screen } from '@testing-library/react'
import { pick } from 'lodash-es'

import { MockLinkComponent } from 'app/components/Link'
import { TestIds } from 'app/constants/testIds'

import { AuthorLink } from './AuthorLink'
import { ORC_ID_URL } from './constants'
import { AuthorInfo } from './types'

const DEFAULT_AUTHOR: AuthorInfo = {
corresponding_author_status: true,
email: '[email protected]',
name: 'Actin Filament',
orcid: '0000-0000-0000-0000',
primary_author_status: false,
}

it('should not be link if orc ID is not provided', () => {
render(<AuthorLink author={pick(DEFAULT_AUTHOR, 'name')} />)
expect(screen.queryByRole('link')).not.toBeInTheDocument()
})

it('should be a link if orc ID is provided', () => {
render(
<AuthorLink
author={pick(DEFAULT_AUTHOR, 'name', 'orcid')}
LinkComponent={MockLinkComponent}
/>,
)

const link = screen.getByRole('link')
expect(link).toBeInTheDocument()
expect(link).toHaveProperty('href', `${ORC_ID_URL}/${DEFAULT_AUTHOR.orcid}`)
expect(screen.getByTestId(TestIds.OrcIdIcon)).toBeInTheDocument()
})

it('should have icon if user is corresponding author', () => {
render(
<AuthorLink
author={pick(DEFAULT_AUTHOR, 'name', 'corresponding_author_status')}
LinkComponent={MockLinkComponent}
/>,
)

expect(screen.getByTestId(TestIds.EnvelopeIcon)).toBeInTheDocument()
})

it('should use regular icon size', () => {
render(<AuthorLink author={pick(DEFAULT_AUTHOR, 'name')} />)

const text = screen.getByText(DEFAULT_AUTHOR.name)
expect(text).toHaveClass('text-xs')
})

it('should use large icon size', () => {
render(<AuthorLink author={pick(DEFAULT_AUTHOR, 'name')} large />)

const text = screen.getByText(DEFAULT_AUTHOR.name)
expect(text).toHaveClass('text-sm')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { LinkProps } from '@remix-run/react'
import { ComponentType } from 'react'

import { EnvelopeIcon, ORCIDIcon } from 'app/components/icons'
import { Link } from 'app/components/Link'
import { cns } from 'app/utils/cns'

import { ORC_ID_URL } from './constants'
import { AuthorInfo } from './types'

const BASE_ICON_SIZE_PX = 10
const LARGE_ICON_SIZE_PX = 14

export function AuthorLink({
author,
large,
LinkComponent = Link,
}: {
author: AuthorInfo
large?: boolean
LinkComponent?: ComponentType<LinkProps>
}) {
const iconSize = large ? LARGE_ICON_SIZE_PX : BASE_ICON_SIZE_PX
const content = (
<span className="inline">
<span
className={cns(
'inline border-b pb-sds-xxxs',

author.orcid
? [
'border-dashed hover:border-solid',

author.primary_author_status
? 'border-black'
: 'border-sds-gray-500',
]
: 'border-transparent',
)}
>
{author.orcid && (
<ORCIDIcon className="inline mb-0.5" width={iconSize} />
)}

<span className={cns('ml-sds-xxxs', large ? 'text-sm' : 'text-xs')}>
{author.name}
</span>
</span>

{author.corresponding_author_status && (
<EnvelopeIcon
className={cns(
'text-sds-gray-400 mx-sds-xxxs',
'mb-2 inline-block h-sds-icon-xs w-sds-icon-xs',
)}
/>
)}
</span>
)

if (author.orcid) {
return (
<LinkComponent to={`${ORC_ID_URL}/${author.orcid}`}>
{content}
</LinkComponent>
)
}

return content
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ORC_ID_URL = 'https://orcid.org'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './AuthorLink'
export * from './AuthorLink.mock'
export * from './types'
10 changes: 10 additions & 0 deletions frontend/packages/data-portal/app/components/AuthorLink/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Dataset_Authors } from 'app/__generated__/graphql'

export type AuthorInfo = Pick<
Dataset_Authors,
| 'corresponding_author_status'
| 'email'
| 'name'
| 'orcid'
| 'primary_author_status'
>
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,7 @@ export function DatasetTable() {
/>
</>
) : (
<DatasetAuthors
authors={dataset.authors}
separator=","
compact
/>
<DatasetAuthors authors={dataset.authors} compact />
)}
</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { render, screen } from '@testing-library/react'

import { AuthorInfo, MockAuthorLink } from 'app/components/AuthorLink'

import { DatasetAuthors } from './DatasetAuthors'

const DEFAULT_AUTHORS: AuthorInfo[] = [
{ name: 'Foo', corresponding_author_status: true },
{ name: 'Bar' },
{ name: 'Foo Bar', primary_author_status: true },
{ name: 'Foobar Foo' },
{ name: 'Foo Bar 2', primary_author_status: true },
{ name: 'Foo 2', corresponding_author_status: true },
{ name: 'Bar 2' },
]

const AUTHOR_MAP = Object.fromEntries(
DEFAULT_AUTHORS.map((author) => [author.name, author]),
)

it('should render authors', () => {
render(<DatasetAuthors authors={DEFAULT_AUTHORS} />)

DEFAULT_AUTHORS.forEach((author) =>
expect(screen.getByText(author.name)).toBeInTheDocument(),
)
})

it('should sort primary authors', () => {
render(<DatasetAuthors authors={DEFAULT_AUTHORS} />)
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')

expect(AUTHOR_MAP[authors[0]].primary_author_status).toBe(true)
expect(AUTHOR_MAP[authors[1]].primary_author_status).toBe(true)
})

it('should sort other authors', () => {
render(<DatasetAuthors authors={DEFAULT_AUTHORS} />)
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')
const otherAuthors = authors.slice(2, -2)

otherAuthors.forEach((author) => {
expect(AUTHOR_MAP[author].primary_author_status).toBeUndefined()
expect(AUTHOR_MAP[author].corresponding_author_status).toBeUndefined()
})
})

it('should sort corresponding authors', () => {
render(<DatasetAuthors authors={DEFAULT_AUTHORS} />)
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')

expect(AUTHOR_MAP[authors.at(-1) ?? ''].corresponding_author_status).toBe(
true,
)
expect(AUTHOR_MAP[authors.at(-2) ?? ''].corresponding_author_status).toBe(
true,
)
})

it('should render author links', () => {
const authors = DEFAULT_AUTHORS.map((author, idx) => ({
...author,
orcid: `0000-0000-0000-000${idx}`,
}))

render(
<DatasetAuthors authors={authors} AuthorLinkComponent={MockAuthorLink} />,
)

authors.forEach((author) =>
expect(
screen.getByRole('link', { name: `${author.name}` }),
).toBeInTheDocument(),
)
})

it('should not render author links when compact', () => {
const authors = DEFAULT_AUTHORS.map((author, idx) => ({
...author,
orcid: `0000-0000-0000-000${idx}`,
}))

render(
<DatasetAuthors
authors={authors}
AuthorLinkComponent={MockAuthorLink}
compact
/>,
)

expect(screen.queryByRole('link')).not.toBeInTheDocument()
})

it('should not render other authors when compact', () => {
render(<DatasetAuthors authors={DEFAULT_AUTHORS} compact />)
const authorNode = screen.getByRole('paragraph')
const authors = (authorNode.textContent ?? '').split(', ')
const otherAuthors = authors.slice(2, -2)

otherAuthors.forEach((author) =>
expect(screen.queryByText(author)).not.toBeInTheDocument(),
)
})

it('should render comma if compact and has corresponding authors', () => {
render(<DatasetAuthors authors={DEFAULT_AUTHORS} compact />)
expect(screen.getByText((text) => text.includes('... ,'))).toBeInTheDocument()
})

it('should not render comma for others if compact and no corresponding authors', () => {
render(
<DatasetAuthors
authors={DEFAULT_AUTHORS.filter(
(author) => !author.corresponding_author_status,
)}
compact
/>,
)

expect(screen.getByText((text) => text.includes('...'))).toBeInTheDocument()
expect(
screen.queryByText((text) => text.includes('... ,')),
).not.toBeInTheDocument()
})
Loading

0 comments on commit 9488f2a

Please sign in to comment.