Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app-project: Add OrganizationLink to ProjectHeader and Hero components #4567

Merged
merged 16 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions packages/app-project/public/locales/en/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"collect": "Collect",
"recents": "Recents",
"admin": "Admin page",
"organization": "From the organization:",
"ApprovedIcon": {
"title": "Zooniverse Approved"
},
Expand Down
25 changes: 21 additions & 4 deletions packages/app-project/src/components/ProjectHeader/ProjectHeader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box } from 'grommet'
import { observer } from 'mobx-react'
import { bool, string } from 'prop-types'
import { bool, shape, string } from 'prop-types'
import styled from 'styled-components'
import { useResizeDetector } from 'react-resize-detector'

Expand All @@ -10,6 +10,7 @@ import {
Background,
DropdownNav,
LocaleSwitcher,
OrganizationLink,
Nav,
ProjectTitle,
UnderReviewLabel
Expand All @@ -22,6 +23,7 @@ const StyledBox = styled(Box)`

function ProjectHeader({
adminMode,
organization,
className = ''
}) {
const { width, height, ref } = useResizeDetector({
Expand All @@ -33,7 +35,6 @@ function ProjectHeader({
inBeta,
title
} = useStores()

const hasTranslations = availableLocales?.length > 1
const maxColumnWidth = hasTranslations ? 1000 : 900
const isNarrow = width < maxColumnWidth
Expand All @@ -47,11 +48,21 @@ function ProjectHeader({
return (
<StyledBox ref={ref} className={className}>
<Background />
{organization?.id ? (
<OrganizationLink
slug={organization.slug}
title={organization.strings?.title || organization.title}
/>
) : null}
<StyledBox
align='center'
direction={direction}
justify='between'
pad='medium'
pad={{
bottom: 'medium',
horizontal: 'medium',
top: organization?.id ? 'none' : 'medium'
}}
>
<Box direction={direction} gap='small'>
<Box
Expand Down Expand Up @@ -98,7 +109,13 @@ ProjectHeader.propTypes = {
/** Zooniverse admin mode */
adminMode: bool,
/** Optional CSS classes */
className: string
className: string,
/** Project organization */
organization: shape({
id: string,
slug: string,
title: string
})
}

export default observer(ProjectHeader)
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ const {
LaunchApproved,
LoggedIn,
MultipleLanguages,
NotLoggedIn
NotLoggedIn,
OrganizationLink
} = composeStories(Stories)

describe('Component > ProjectHeader', function () {
let languageButton, navMenu, projectAvatar, projectBackground, projectTitle
let languageButton, navMenu, projectAvatar, projectBackground, projectTitle, organizationLink

before(function () {
nock('https://talk-staging.zooniverse.org')
Expand All @@ -40,6 +41,7 @@ describe('Component > ProjectHeader', function () {
projectTitle = screen.getByRole('heading', { level: 1, name: 'Snapshot Serengeti' })
navMenu = screen.getByRole('navigation', { name: 'ProjectHeader.ProjectNav.ariaLabel' })
languageButton = screen.queryByRole('button', { name: 'ProjectHeader.LocaleSwitcher.label'})
organizationLink = screen.queryByRole('link', { name: 'Snapshot Safari' })
})

it('should display the project title', function () {
Expand All @@ -61,6 +63,10 @@ describe('Component > ProjectHeader', function () {
it('should not show the language menu button', function () {
expect(languageButton).to.be.null()
})

it('should not show the organization link', function () {
expect(organizationLink).to.be.null()
})
})

describe('when not logged in', function () {
Expand Down Expand Up @@ -165,4 +171,17 @@ describe('Component > ProjectHeader', function () {
expect(languageButton).to.exist()
})
})

describe('with an organization', function () {
let organizationLink

before(function () {
render(<OrganizationLink />)
organizationLink = screen.getByRole('link', { name: 'Snapshot Safari' })
})

it('should show the organization link', async function () {
expect(organizationLink).to.exist()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const mockRouter = {
}
}

const ORGANIZATION = {
id: '1',
listed: true,
slug: 'zooniverse/snapshot-safari',
title: 'Snapshot Safari'
}

export default {
title: 'Project App / Shared / Project Header',
component: ProjectHeader
Expand Down Expand Up @@ -302,3 +309,38 @@ MultipleLanguages.args = {
}
}
}

export function OrganizationLink({ project }) {
const snapshot = { project }
applySnapshot(OrganizationLink.store, snapshot)
return (
<RouterContext.Provider value={mockRouter}>
<Provider store={OrganizationLink.store}>
<ProjectHeader organization={ORGANIZATION} />
</Provider>
</RouterContext.Provider>
)
}
OrganizationLink.store = initStore(true)
OrganizationLink.args = {
adminMode: false,
className: '',
project: {
avatar: {
src: 'https://panoptes-uploads.zooniverse.org/project_avatar/442e8392-6c46-4481-8ba3-11c6613fba56.jpeg'
},
background: {
src: 'https://panoptes-uploads.zooniverse.org/project_background/7a3c6210-f97d-4f40-9ab4-8da30772ee01.jpeg'
},
configuration: {
languages: ['en']
},
slug: 'zooniverse/snapshot-serengeti',
strings: {
display_name: 'Snapshot Serengeti'
},
links: {
active_workflows: ['1']
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Anchor, Box } from 'grommet'
import { string } from 'prop-types'
import styled from 'styled-components'
import { useTranslation } from 'next-i18next'
import { SpacedText } from '@zooniverse/react-components'

const StyledBox = styled(Box)`
position: relative;
`

/**
Link text styles
*/
const StyledSpacedText = styled(SpacedText)`
text-shadow: 0 2px 2px rgba(0, 0, 0, 0.22);
`

/**
Link styles
*/
const StyledAnchor = styled(Anchor)`
border-bottom: 3px solid transparent;
white-space: nowrap;

&:hover {
text-decoration: none;
border-bottom: 3px solid white;
}
`

function OrganizationLink({
slug = '',
title = ''
}) {
const { t } = useTranslation('components')

return (
<StyledBox
align='baseline'
alignSelf='end'
direction='row'
gap='xsmall'
pad={{
horizontal: 'medium',
top: 'medium'
}}
>
<StyledSpacedText
color='light-3'
>
{t('ProjectHeader.organization')}
</StyledSpacedText>
<StyledAnchor
href={`/organizations/${slug}`}
>
<StyledSpacedText
color='white'
weight='bold'
>
{title}
</StyledSpacedText>
</StyledAnchor>
</StyledBox>
)
}

OrganizationLink.propTypes = {
/** The organization slug */
slug: string,
/** The organization name */
title: string
}
export default OrganizationLink
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './OrganizationLink'
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export { default as Background } from './Background'
export { default as DropdownNav } from './DropdownNav'
export { default as LocaleSwitcher } from './LocaleSwitcher'
export { default as Nav } from './Nav'
export { default as OrganizationLink } from './OrganizationLink'
export { default as ProjectTitle } from './ProjectTitle'
export { default as UnderReviewLabel } from './UnderReviewLabel'
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { panoptes } from '@zooniverse/panoptes-js'

import fetchTranslations from '@helpers/fetchTranslations'
import getServerSideAPIHost from '@helpers/getServerSideAPIHost'
import logToSentry from '@helpers/logger/logToSentry.js'

async function fetchOrganizationData(organizationID, env) {
const { headers, host } = getServerSideAPIHost(env)
try {
const query = {
env,
id: organizationID,
listed: true
}
const response = await panoptes.get('/organizations', query, { ...headers }, host)
const [ organization ] = response.body.organizations
return organization
} catch (error) {
logToSentry(error)
console.log('Error loading organization:', error)
}
}

async function fetchOrganization(organizationID, locale, env) {
const organization = await fetchOrganizationData(organizationID, env)
if (!organization) return null

const translation = await fetchTranslations({
translated_id: organizationID,
translated_type: 'organization',
fallback: organization?.primary_language,
language: locale,
env
})

return {
...organization,
strings: translation?.strings || null
}
}

export default fetchOrganization
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { expect } from 'chai'
import nock from 'nock'

import fetchOrganization from './fetchOrganization'

describe('Helpers > fetchOrganization', function () {
const ORGANIZATION = {
id: '456',
display_name: 'Test Organization',
listed: true,
primary_language: 'en',
title: 'Test Organization',
}

const TRANSLATIONS = [
{
language: 'en',
strings: {
title: 'Test Organization'
}
},
{
language: 'fr',
strings: {
title: 'traduction française'
}
}
]

before(function () {
nock('https://panoptes-staging.zooniverse.org/api')
.persist()
.get('/organizations')
.query(true)
.reply(200, {
organizations: [ORGANIZATION]
})
.get('/translations')
.query(true)
.reply(200, {
translations: TRANSLATIONS
})
})

after(function () {
nock.cleanAll()
})

it('should provide the expected result', async function () {
const result = await fetchOrganization('456', 'en', 'staging')

expect(result).to.deep.equal({
...ORGANIZATION,
strings: TRANSLATIONS[0].strings
})
})

describe('with an existing translation', function () {
it('should return the translated organization', async function () {
const result = await fetchOrganization('456', 'fr', 'staging')

expect(result).to.deep.equal({
...ORGANIZATION,
strings: TRANSLATIONS[1].strings
})
})
})

describe('with an error', function () {
it('should return null', async function () {
const mockError = new Error('API is down')
const scope = nock('https://panoptes-staging.zooniverse.org/api')
.get('/organizations')
.query(true)
.replyWithError(mockError)
.get('/translations')
.query(true)
.reply(200, {
translations: TRANSLATIONS
})
const result = await fetchOrganization('456', 'en', 'staging')

expect(result).to.be.null
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './fetchOrganization'
Loading