Skip to content

Commit

Permalink
Add support for inactive (private) workflows (#2940)
Browse files Browse the repository at this point in the history
- swap `project.links.workflows` for `project.links.active_workflows` when validating workflow IDs in `app-project`.
- add a `useProjectRoles` hook to the classifier, which gets roles for a project, user and token.
- check project roles during workflow selection. Allow owners, testers and collaborators to select any project workflow.
- add a usePanoptesUser hook.
- add a usePanoptesAuth hook.
- move `useStores` to the hooks directory.
  • Loading branch information
eatyourgreens authored Mar 31, 2022
1 parent 2994b07 commit daa07b7
Show file tree
Hide file tree
Showing 22 changed files with 155 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { panoptes } from '@zooniverse/panoptes-js'

import { logToSentry } from '@helpers/logger'

async function fetchWorkflowData (activeWorkflows, env) {
async function fetchWorkflowData(workflows, env) {
try {
const query = {
complete: false,
env,
fields: 'completeness,configuration,display_name,grouped,prioritized',
id: activeWorkflows.join(',')
id: workflows.join(',')
}
const response = await panoptes.get('/workflows', query)
return response.body.workflows
Expand All @@ -18,7 +18,7 @@ async function fetchWorkflowData (activeWorkflows, env) {
}
}

async function fetchSingleWorkflow (workflowID, env) {
async function fetchSingleWorkflow(workflowID, env) {
try {
const query = {
env,
Expand All @@ -34,14 +34,14 @@ async function fetchSingleWorkflow (workflowID, env) {
}
}

async function fetchDisplayNames (language, activeWorkflows, env) {
async function fetchDisplayNames(language, workflows, env) {
let displayNames = {}
try {
const response = await panoptes.get('/translations', {
env,
fields: 'strings,translated_id',
language,
'translated_id': activeWorkflows.join(','),
'translated_id': workflows.join(','),
'translated_type': 'workflow'
})
const { translations } = response.body
Expand Down Expand Up @@ -88,29 +88,29 @@ async function fetchWorkflowsHelper(
/* the current locale */
language = 'en',
/* an array of workflow IDs to fetch */
activeWorkflowIDs,
workflowIDs,
/* a specific workflow ID to fetch */
workflowID,
/* display order of workflow IDs, specified by the project owner. */
workflowOrder = [],
/* API environment, production | staging. */
env
) {
const activeWorkflows = await fetchWorkflowData(activeWorkflowIDs, env)
const workflows = await fetchWorkflowData(workflowIDs, env)
if (workflowID) {
const activeWorkflow = activeWorkflows.find(workflow => workflow.id === workflowID)
const activeWorkflow = workflows.find(workflow => workflow.id === workflowID)
if (!activeWorkflow) {
/*
Always fetch specified workflows, even if they're complete.
*/
const workflow = await fetchSingleWorkflow(workflowID, env)
activeWorkflows.push(workflow)
workflows.push(workflow)
}
}
const workflowIds = activeWorkflows.map(workflow => workflow.id)
const workflowIds = workflows.map(workflow => workflow.id)
const displayNames = await fetchDisplayNames(language, workflowIds, env)

const awaitWorkflows = activeWorkflows.map(workflow => {
const awaitWorkflows = workflows.map(workflow => {
const displayName = displayNames[workflow.id] || workflow.display_name
return buildWorkflow(workflow, displayName, env)
})
Expand All @@ -120,7 +120,7 @@ async function fetchWorkflowsHelper(
return orderedWorkflows
}

function createDisplayNamesMap (translations) {
function createDisplayNamesMap(translations) {
const map = {}
translations.forEach(translation => {
const workflowId = translation.translated_id.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ describe('Helpers > getDefaultPageProps', function () {
primary_language: 'en',
slug: 'test-owner/test-project',
links: {
active_workflows: ['1']
active_workflows: ['1'],
workflows: ['1', '2']
}
}

Expand All @@ -19,7 +20,8 @@ describe('Helpers > getDefaultPageProps', function () {
primary_language: 'en',
slug: 'test-owner/grouped-project',
links: {
active_workflows: ['2']
active_workflows: ['2'],
workflows: ['1', '2']
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@ export default async function getStaticPageProps({ params, query }) {
*/
const { project } = getSnapshot(store)
const language = project.primary_language
const { active_workflows } = project.links

const workflowOrder = project.configuration?.workflow_order || []
/*
Validate any workflow URLs
Validate any workflow URLs against a project's linked workflows
*/
const workflowExists = active_workflows.includes(workflowID)
const workflowExists = project.links.workflows.includes(workflowID)
if (workflowID && !workflowExists) {
const { props } = notFoundError(`Workflow ${workflowID} was not found`)
props.project = project
Expand All @@ -61,7 +60,7 @@ export default async function getStaticPageProps({ params, query }) {
/*
Fetch the active project workflows
*/
const workflows = await fetchWorkflowsHelper(language, active_workflows, workflowID, workflowOrder, env)
const workflows = await fetchWorkflowsHelper(language, project.links.active_workflows, workflowID, workflowOrder, env)
const props = {
project,
workflows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ describe('Helpers > getStaticPageProps', function () {
researcher_quote: null,
slug: 'test-owner/test-project-single-active-workflow',
links: {
active_workflows: ['1']
active_workflows: ['1'],
workflows: ['1', '2']
}
}

Expand All @@ -20,7 +21,8 @@ describe('Helpers > getStaticPageProps', function () {
researcher_quote: null,
slug: 'test-owner/test-project-multiple-active-workflows',
links: {
active_workflows: ['1', '2']
active_workflows: ['1', '2'],
workflows: ['1', '2']
}
}

Expand All @@ -29,7 +31,8 @@ describe('Helpers > getStaticPageProps', function () {
primary_language: 'en',
slug: 'test-owner/grouped-project',
links: {
active_workflows: ['2']
active_workflows: ['2'],
workflows: ['1', '2']
}
}

Expand Down Expand Up @@ -370,7 +373,7 @@ describe('Helpers > getStaticPageProps', function () {
const params = {
owner: 'test-owner',
project: 'test-project-single-active-workflow',
workflowID: '2'
workflowID: '3'
}
const query = {
env: 'production'
Expand All @@ -384,7 +387,7 @@ describe('Helpers > getStaticPageProps', function () {
})

it('should return a workflow error message', function () {
expect(props.title).to.equal('Workflow 2 was not found')
expect(props.title).to.equal('Workflow 3 was not found')
})
})
})
Expand Down
13 changes: 10 additions & 3 deletions packages/lib-classifier/src/components/Classifier/Classifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import zooTheme from '@zooniverse/grommet-theme'
import '../../translations/i18n'
import i18n from 'i18next'


import { usePanoptesUser, useProjectRoles } from '@hooks'
import Layout from './components/Layout'
import ModalTutorial from './components/ModalTutorial'

Expand All @@ -22,6 +22,13 @@ export default function Classifier({
workflowID
}) {

const user = usePanoptesUser()
const projectRoles = useProjectRoles(project?.id, user?.id)

const canPreviewWorkflows = projectRoles.indexOf('owner') > -1 ||
projectRoles.indexOf('collaborator') > -1 ||
projectRoles.indexOf('tester') > -1

useEffect(function onLocaleChange() {
if (locale) {
classifierStore.setLocale(locale)
Expand All @@ -39,9 +46,9 @@ export default function Classifier({
const { workflows } = classifierStore
if (workflowID) {
console.log('starting new subject queue', { workflowID, subjectSetID, subjectID })
workflows.selectWorkflow(workflowID, subjectSetID, subjectID)
workflows.selectWorkflow(workflowID, subjectSetID, subjectID, canPreviewWorkflows)
}
}, [subjectID, subjectSetID, workflowID])
}, [canPreviewWorkflows, subjectID, subjectSetID, workflowID])

/*
This should run when a project owner edits a workflow, but not when a workflow updates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export default function ClassifierContainer({
subjectSetID,
workflowID
}) {

const [loaded, setLoaded] = useState(false)
const storeEnvironment = { authClient, client }

Expand Down
1 change: 0 additions & 1 deletion packages/lib-classifier/src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export { default as shownMarks } from './shownMarks'
export { default as subjectsSeenThisSession } from './subjectsSeenThisSession'
export { default as subjectViewers } from './subjectViewers'
export { default as useJSONData } from './useJSONData'
export { default as useStores } from './useStores'
export { default as validateMimeType } from './validateMimeType'
export { default as validateSubjectLocations } from './validateSubjectLocations'
export { default as withStores } from './withStores'
17 changes: 0 additions & 17 deletions packages/lib-classifier/src/helpers/useStores/README.md

This file was deleted.

1 change: 0 additions & 1 deletion packages/lib-classifier/src/helpers/useStores/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { observer } from 'mobx-react'
import useStores from '../useStores'
import { useStores } from '@hooks'
/**
Connect a view component to the store, when all you want to do is map some store properties to component props, without any internal state in the connector component.
*/
Expand Down
39 changes: 39 additions & 0 deletions packages/lib-classifier/src/hooks/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@ Returns the new store when hydration is complete. Snapshots are stored in sessio
```js
const classifierStore = useHydratedStore({ authClient, client }, cachePanoptesData = false, storageKey)
```
## usePanoptesAuth

Asynchronously fetch an auth token, for a given user ID. A wrapper for `authClient.checkBearerToken()`.

```js
const authorization = usePanoptesAuth(user.id)
```

## usePanoptesUser

Get the logged-in user, or null if no one is logged in.

```js
const user = usePanoptesUser()
```

## useProjectRoles

Get the logged-in user's project roles, as an array of strings, or an empty array if no one is logged in.

```js
const projectRoles = useProjectRoles(project.id, user.id)
```

# useStores

A custom hook which connects a component to the classifier store, or to a filtered list of store properties if a store mapper function is provided.

Usage:
```js
function storeMapper(store) {
const { workflows } = store
return { workflows }
}

function MyConnectedComponent(props) {
const { workflows } = useStores(storeMapper)
}
```

## useWorkflowSnapshot

Expand Down
4 changes: 4 additions & 0 deletions packages/lib-classifier/src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export { default as useHydratedStore } from './useHydratedStore'
export { default as usePanoptesAuth } from './usePanoptesAuth'
export { default as usePanoptesUser } from './usePanoptesUser'
export { default as useProjectRoles } from './useProjectRoles'
export { default as useStores } from './useStores'
export { default as useWorkflowSnapshot } from './useWorkflowSnapshot'
19 changes: 19 additions & 0 deletions packages/lib-classifier/src/hooks/usePanoptesAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useState } from 'react'

import { useStores } from '@hooks'
import { getBearerToken } from '@store/utils'

export default function usePanoptesAuth(userID) {
const { authClient } = useStores()
const [authorization, setAuthorization] = useState()
async function checkAuth() {
const token = await getBearerToken(authClient)
setAuthorization(token)
}

useEffect(function onUserChange() {
checkAuth()
}, [userID])

return authorization
}
17 changes: 17 additions & 0 deletions packages/lib-classifier/src/hooks/usePanoptesUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import useSWR from 'swr'

import { useStores } from '@hooks'

const SWRoptions = {
revalidateIfStale: true,
revalidateOnMount: true,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0
}

export default function usePanoptesUser() {
const { authClient } = useStores()
const { data } = useSWR('/me', authClient.checkCurrent, SWRoptions)
return data
}
27 changes: 27 additions & 0 deletions packages/lib-classifier/src/hooks/useProjectRoles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import useSWR from 'swr'
import { panoptes } from '@zooniverse/panoptes-js'

import { usePanoptesAuth } from './'

const SWRoptions = {
revalidateIfStale: true,
revalidateOnMount: true,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0
}

async function fetchProjectRoles({ project_id, user_id, authorization }) {
if (authorization) {
const { body } = await panoptes.get(`/project_roles`, { project_id, user_id }, { authorization })
const [projectRoles] = body.project_roles
return projectRoles.roles
}
return []
}

export default function useProjectRoles(project_id, user_id) {
const authorization = usePanoptesAuth(user_id)
const { data } = useSWR({ project_id, user_id, authorization }, fetchProjectRoles, SWRoptions)
return data ?? []
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { renderHook } from '@testing-library/react-hooks/pure'
import { Provider } from 'mobx-react'

import mockStore from '@test/mockStore'
import useStores from '.'
import { useStores } from '.'

describe('Helpers > useStores', function () {
describe('Hooks > useStores', function () {
describe('without storeMapper', function () {
let current
let store
Expand Down
1 change: 0 additions & 1 deletion packages/lib-classifier/src/hooks/useWorkflowSnapshot.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import useSWR from 'swr'

import { panoptes } from '@zooniverse/panoptes-js'

const SWRoptions = {
Expand Down
Loading

0 comments on commit daa07b7

Please sign in to comment.