Skip to content

Commit

Permalink
Add support for inactive (private) workflows
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.
  • Loading branch information
eatyourgreens committed Mar 24, 2022
1 parent 5fbdb31 commit bb9fbd8
Show file tree
Hide file tree
Showing 16 changed files with 136 additions and 34 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
10 changes: 8 additions & 2 deletions packages/lib-classifier/src/components/Classifier/Classifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ export default function Classifier({
onError = () => true,
project,
showTutorial = false,
projectRoles = [],
subjectID,
subjectSetID,
workflowSnapshot,
workflowVersion,
workflowID
}) {

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 +44,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 Expand Up @@ -80,6 +85,7 @@ Classifier.propTypes = {
classifierStore: PropTypes.object.isRequired,
locale: PropTypes.string,
onError: PropTypes.func,
projectRoles: PropTypes.arrayOf(PropTypes.string),
project: PropTypes.shape({
id: PropTypes.string.isRequired
}).isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
tutorials as tutorialsClient
} from '@zooniverse/panoptes-js'

import { useHydratedStore, useWorkflowSnapshot } from '@hooks'
import { useHydratedStore, usePanoptesUser, useProjectRoles, useWorkflowSnapshot } from '@hooks'
import { unregisterWorkers } from '../../workers'
import Classifier from './Classifier'

Expand Down Expand Up @@ -61,10 +61,11 @@ export default function ClassifierContainer({
subjectSetID,
workflowID
}) {

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

const user = usePanoptesUser(authClient)
const projectRoles = useProjectRoles(project?.id, user?.id, authClient)
const workflowSnapshot = useWorkflowSnapshot(workflowID)

const classifierStore = useHydratedStore(storeEnvironment, cachePanoptesData, `fem-classifier-${project.id}`)
Expand Down Expand Up @@ -128,6 +129,7 @@ export default function ClassifierContainer({
onError={onError}
project={project}
showTutorial={showTutorial}
projectRoles={projectRoles}
subjectSetID={subjectSetID}
subjectID={subjectID}
workflowSnapshot={workflowSnapshot}
Expand Down
23 changes: 23 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,29 @@ 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 and OAuth client. A wrapper for `authClient.checkBearerToken()`.

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

## usePanoptesUser

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

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

## 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, authClient)
```

## useWorkflowSnapshot

Expand Down
3 changes: 3 additions & 0 deletions packages/lib-classifier/src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
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 useWorkflowSnapshot } from './useWorkflowSnapshot'
16 changes: 16 additions & 0 deletions packages/lib-classifier/src/hooks/usePanoptesAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useState } from 'react'
import { getBearerToken } from '@store/utils'

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

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

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

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

async function fetchUser(authClient) {
const user = await authClient.checkCurrent()
return user
}

export default function usePanoptesUser(authClient) {
const { data } = useSWR(authClient, fetchUser, 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, authClient) {
const authorization = usePanoptesAuth(user_id, authClient)
const { data } = useSWR({ project_id, user_id, authorization }, fetchProjectRoles, SWRoptions)
return data ?? []
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ const WorkflowStore = types

.actions(self => {

function * selectWorkflow (id = self.defaultWorkflowID, subjectSetID, subjectID) {
function * selectWorkflow(id = self.defaultWorkflowID, subjectSetID, subjectID, canPreviewWorkflows = false) {
if (!id) {
throw new ReferenceError('No workflow ID available')
}
const availableWorkflows = canPreviewWorkflows ?
self.project?.links?.workflows :
self.project?.links?.active_workflows

const { subjects } = getRoot(self)
const activeWorkflow = tryReference(() => self.active)
const activeSubjectSet = activeWorkflow?.subjectSet
Expand All @@ -44,10 +48,9 @@ const WorkflowStore = types
subjectChanged

if (shouldReload) {
const activeWorkflows = self.project?.links?.active_workflows || []
const projectID = self.project?.id
let selectedSubjects
if (activeWorkflows.indexOf(id) > -1) {
if (availableWorkflows?.indexOf(id) > -1) {
const workflow = yield self.getResource(id)
self.resources.put(workflow)
const selectedWorkflow = self.resources.get(id)
Expand Down
2 changes: 1 addition & 1 deletion packages/lib-classifier/test/factories/ProjectFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default new Factory()
.attr('links', ['activeWorkflowId', 'workflowId'], (activeWorkflowId, workflowId) => {
return {
active_workflows: [activeWorkflowId],
workflows: [workflowId]
workflows: [activeWorkflowId, workflowId]
}
})
.attr('slug', 'zooniverse/example')
Loading

0 comments on commit bb9fbd8

Please sign in to comment.