Skip to content

Commit

Permalink
feat(app-project): remember current workflow in a cookie
Browse files Browse the repository at this point in the history
- Store your selected workflow in a `workflow_id` session cookie on the `zooniverse.org` domain, scoped to the current project.
- Set that cookie when you choose a workflow with the `WorkflowSelectButton` component.
- Read the cookie in the Project model, and store it as `project.selectedWorkflow`. Fall back to `project.defaultWorkflow` for backwards compatibility.
- Use `project.defaultWorkflow` on the Classify page, if there's no workflow in the URL.
- Use `props.workflowID` on the Classify page, if there is a workflow in the URL.
- Add the project context to tests.
  • Loading branch information
eatyourgreens committed Aug 17, 2024
1 parent 22707c9 commit 7ebaf77
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 113 deletions.
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
Expand Up @@ -6,14 +6,18 @@ 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')
}
}
Expand All @@ -23,16 +27,23 @@ function ClassifyPageConnector(props) {
const {
appLoadingState,
projectPreferences,
defaultWorkflow,
setSelectedWorkflow,
workflowAssignmentEnabled = false
} = useStore(store)

if (props.workflowID && props.workflowID !== defaultWorkflow) {
setSelectedWorkflow(props.workflowID)
}

return (
<ClassifyPageContainer
{...props}
appLoadingState={appLoadingState}
assignedWorkflowID={projectPreferences?.settings?.workflow_id}
projectPreferences={projectPreferences}
workflowAssignmentEnabled={workflowAssignmentEnabled}
{...props}
workflowID={props.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

0 comments on commit 7ebaf77

Please sign in to comment.