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

feat(app-project): remember current workflow in a same-site session cookie #6216

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SpacedText } from '@zooniverse/react-components'
import { Anchor, Box } from 'grommet'
import { observer } from 'mobx-react'
import { useRouter } from 'next/router'
import { bool } from 'prop-types'
import styled, { css } from 'styled-components'
Expand Down Expand Up @@ -84,4 +85,4 @@ Nav.propTypes = {
adminMode: bool
}

export default Nav
export default observer(Nav)
Original file line number Diff line number Diff line change
@@ -1,38 +1,62 @@
import { MobXProviderContext, observer } from 'mobx-react'
import { useRouter } from 'next/router'
import { useContext } from 'react'
import ClassifyPageContainer from './ClassifyPageContainer'

function useStore(store) {
const {
appLoadingState,
project: {
experimental_tools
experimental_tools,
defaultWorkflow,
setSelectedWorkflow
},
user: { personalization: { projectPreferences } }
} = store

return {
appLoadingState,
projectPreferences,
defaultWorkflow,
setSelectedWorkflow,
workflowAssignmentEnabled: experimental_tools.includes('workflow assignment')
}
}

function ClassifyPageConnector(props) {
function ClassifyPageConnector({
// workflow ID from the page URL
workflowID,
...props
}) {
const router = useRouter()
const { store } = useContext(MobXProviderContext)
const {
appLoadingState,
projectPreferences,
defaultWorkflow,
setSelectedWorkflow,
workflowAssignmentEnabled = false
} = useStore(store)

// store the workflow from the URL, if it isn't already stored
if (workflowID && workflowID !== defaultWorkflow) {
setSelectedWorkflow(workflowID)
}

// client-side redirect if there's no workflow in the URL
if (!workflowID && defaultWorkflow) {
const newPath = router.asPath.replace('/classify', `/classify/workflow/${defaultWorkflow}`)
router.replace(newPath, newPath, { shallow: true })
}

return (
<ClassifyPageContainer
{...props}
appLoadingState={appLoadingState}
assignedWorkflowID={projectPreferences?.settings?.workflow_id}
projectPreferences={projectPreferences}
workflowAssignmentEnabled={workflowAssignmentEnabled}
{...props}
workflowID={workflowID || defaultWorkflow}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
import { MobXProviderContext, observer } from 'mobx-react'
import { Button, Box, CheckBox } from 'grommet'
import { Modal, PrimaryButton, SpacedText } from '@zooniverse/react-components'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import addQueryParams from '@helpers/addQueryParams'
Expand All @@ -12,6 +11,8 @@ function useStore() {
const { store } = useContext(MobXProviderContext)

return {
/** the current project */
project: store.project,
/** assignedWorkflowID is fetched every 5 classifications per user session */
assignedWorkflowID: store.user.personalization.projectPreferences.settings?.workflow_id,
/** This function determines if the user has an assigned workflow and verifies that workflow is active in panoptes */
Expand All @@ -20,13 +21,10 @@ function useStore() {
}

function WorkflowAssignmentModal({ currentWorkflowID = '' }) {
const { assignedWorkflowID, promptAssignment } = useStore()
const { project, assignedWorkflowID, promptAssignment } = useStore()

const { t } = useTranslation('screens')
const router = useRouter()
const owner = router?.query?.owner
const project = router?.query?.project
const url = `/${owner}/${project}/classify/workflow/${assignedWorkflowID}`
const url = `/${project.slug}/classify/workflow/${assignedWorkflowID}`

/** Check if user has dismissed the modal, but only in the browser */
const isBrowser = typeof window !== 'undefined'
Expand Down Expand Up @@ -66,6 +64,10 @@ function WorkflowAssignmentModal({ currentWorkflowID = '' }) {
setActive(false)
}

function onClick() {
project.setSelectedWorkflow(assignedWorkflowID)
}

return (
<Modal
active={active}
Expand Down Expand Up @@ -95,6 +97,7 @@ function WorkflowAssignmentModal({ currentWorkflowID = '' }) {
as={Link}
href={addQueryParams(url)}
label={t('Classify.WorkflowAssignmentModal.confirm')}
onClick={onClick}
/>
</Box>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const mockRouter = {

const snapshot = {
project: {
slug: 'zooniverse/snapshot-serengeti',
strings: {
display_name: 'Snapshot Serengeti',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import asyncStates from '@zooniverse/async-states'
import { mount } from 'enzyme'
import { Provider } from 'mobx-react'
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime'

import initStore from '@stores'
import WorkflowSelector from './WorkflowSelector'
import WorkflowSelectButtons from './components/WorkflowSelectButtons'
import { expect } from 'chai'

describe('Component > WorkflowSelector', function () {
let store

const mockRouter = {
asPath: '/zooniverse/snapshot-serengeti/about/team',
basePath: '/projects',
Expand Down Expand Up @@ -42,54 +46,66 @@ describe('Component > WorkflowSelector', function () {
const DEFAULT_WORKFLOW_DESCRIPTION = 'WorkflowSelector.message'
/** The translation function will simply return keys in a testing env */

it('should render without crashing', function () {
const wrapper = mount(
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
theme={THEME}
workflows={WORKFLOWS}
workflowDescription={WORKFLOW_DESCRIPTION}
/>
</RouterContext.Provider>
)
expect(wrapper).to.be.ok()
this.beforeEach(function () {
store = initStore(true)
})

describe('workflow description', function () {
it('should use the `workflowDescription` prop if available', function () {
const wrapper = mount(
it('should render without crashing', function () {
const wrapper = mount(
<Provider store={store}>
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
theme={THEME}
workflows={WORKFLOWS}
workflowDescription={WORKFLOW_DESCRIPTION}
/>
</RouterContext.Provider>
</Provider>
)
expect(wrapper).to.be.ok()
})

describe('workflow description', function () {
it('should use the `workflowDescription` prop if available', function () {
const wrapper = mount(
<Provider store={store}>
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
theme={THEME}
workflows={WORKFLOWS}
workflowDescription={WORKFLOW_DESCRIPTION}
/>
</RouterContext.Provider>
</Provider>
)
expect(wrapper.contains(WORKFLOW_DESCRIPTION)).to.be.true()
})

it('should use the default message if the `workflowDescription` prop is unset', function () {
const wrapper = mount(
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
theme={THEME}
workflows={WORKFLOWS}
/>
</RouterContext.Provider>
<Provider store={store}>
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
theme={THEME}
workflows={WORKFLOWS}
/>
</RouterContext.Provider>
</Provider>
)
expect(wrapper.contains(DEFAULT_WORKFLOW_DESCRIPTION)).to.be.true()
})

it('should use the default message if the `workflowDescription` prop is an empty string', function () {
const wrapper = mount(
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
theme={THEME}
workflows={WORKFLOWS}
workflowDescription=''
/>
</RouterContext.Provider>
<Provider store={store}>
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
theme={THEME}
workflows={WORKFLOWS}
workflowDescription=''
/>
</RouterContext.Provider>
</Provider>
)
expect(wrapper.contains(DEFAULT_WORKFLOW_DESCRIPTION)).to.be.true()
})
Expand All @@ -98,14 +114,16 @@ describe('Component > WorkflowSelector', function () {
describe('when successfully loaded the user state and loaded the user project preferences', function () {
it('should render workflow select buttons', function () {
const wrapper = mount(
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
uppLoaded={true}
userReadyState={asyncStates.success}
theme={THEME}
workflows={WORKFLOWS}
/>
</RouterContext.Provider>
<Provider store={store}>
<RouterContext.Provider value={mockRouter}>
<WorkflowSelector
uppLoaded={true}
userReadyState={asyncStates.success}
theme={THEME}
workflows={WORKFLOWS}
/>
</RouterContext.Provider>
</Provider>
)
expect(wrapper.find(WorkflowSelectButtons)).to.have.lengthOf(1)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,33 @@ import withThemeContext from '@zooniverse/react-components/helpers/withThemeCont
import { Button } from 'grommet'
import { Next } from 'grommet-icons'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { bool, number, object, shape, string } from 'prop-types'
import { useTranslation } from 'next-i18next'
import { useCallback, useContext } from 'react'
import { MobXProviderContext } from 'mobx-react'

import addQueryParams from '@helpers/addQueryParams'
import theme from './theme'

export const ThemedButton = withThemeContext(Button, theme)

function useProject() {
const stores = useContext(MobXProviderContext)
const { project } = stores.store
return project
}
function WorkflowSelectButton ({
disabled = false,
router,
workflow,
...rest
}) {
const { t } = useTranslation('components')
const nextRouter = useRouter()
router = router || nextRouter
const owner = router?.query?.owner
const project = router?.query?.project
const project = useProject()

const url = `/${owner}/${project}/classify/workflow/${workflow.id}`
const url = `/${project.slug}/classify/workflow/${workflow.id}`
const onClick = useCallback(() => {
project.setSelectedWorkflow(workflow.id)
}, [project, workflow.id])

const href = addQueryParams(url)
const completeness = parseInt(workflow.completeness * 100, 10)
Expand Down Expand Up @@ -68,6 +73,7 @@ function WorkflowSelectButton ({
icon={<Next size='15px' />}
reverse
label={label}
onClick={onClick}
primary
{...rest}
/>
Expand Down
Loading