use context hook for outcome components

closes OUT-4193
flag=improved_outcomes_management
flag=account_level_mastery_scales

test-plan:
- enable flags
- verify clicking through mastery scale, mastery
calculation, and improved outcome management
components and updating data works as expected

Change-Id: Ib4210424709c2eb6952bf197f21c7d93e2629090
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/260282
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Manoel Quirino <manoel.quirino@instructure.com>
Reviewed-by: Michael Brewer-Davis <mbd@instructure.com>
QA-Review: Martin Yosifov <martin.yosifov@instructure.com>
Product-Review: Jody Sailor
This commit is contained in:
Pat Renner 2021-03-09 15:56:18 -06:00
parent e36dcc8474
commit 5839e1491c
18 changed files with 125 additions and 128 deletions

View File

@ -24,7 +24,7 @@ import {View} from '@instructure/ui-view'
import {Billboard} from '@instructure/ui-billboard'
import {PresentationContent} from '@instructure/ui-a11y'
import SVGWrapper from '../shared/SVGWrapper'
import {useCanvasContext} from './shared/hooks'
import useCanvasContext from './shared/hooks/useCanvasContext'
const FindOutcomesBillboard = () => {
const {contextType} = useCanvasContext()

View File

@ -30,7 +30,7 @@ import TreeBrowser from './Management/TreeBrowser'
import FindOutcomesBillboard from './FindOutcomesBillboard'
import FindOutcomesView from './FindOutcomesView'
import {useFindOutcomeModal, ACCOUNT_FOLDER_ID} from './shared/treeBrowser'
import {useCanvasContext} from './shared/hooks'
import useCanvasContext from './shared/hooks/useCanvasContext'
import useGroupDetail from './shared/hooks/useGroupDetail'
const FindOutcomesModal = ({open, onCloseHandler}) => {

View File

@ -24,7 +24,7 @@ import {Text} from '@instructure/ui-text'
import {Button} from '@instructure/ui-buttons'
import {View} from '@instructure/ui-view'
import Modal from '../../shared/components/InstuiModal'
import {useCanvasContext} from '../shared/hooks'
import useCanvasContext from '../shared/hooks/useCanvasContext'
import {showFlashAlert} from '../../shared/FlashAlert'
import {removeOutcomeGroup} from './api'

View File

@ -26,7 +26,7 @@ import {Spinner} from '@instructure/ui-spinner'
import Modal from '../../shared/components/InstuiModal'
import TreeBrowser from './TreeBrowser'
import {useGroupMoveModal} from 'jsx/outcomes/shared/treeBrowser'
import {useCanvasContext} from '../shared/hooks'
import useCanvasContext from '../shared/hooks/useCanvasContext'
const noop = () => {}

View File

@ -17,10 +17,11 @@
*/
import React from 'react'
import OutcomeManagementPanel from '.'
import {createCache} from 'jsx/canvas-apollo'
import {accountMocks} from './__tests__/mocks'
import {MockedProvider} from '@apollo/react-testing'
import {accountMocks} from './__tests__/mocks'
import {createCache} from 'jsx/canvas-apollo'
import OutcomesContext from '../contexts/OutcomesContext'
import OutcomeManagementPanel from '.'
export default {
title: 'Examples/Outcomes/OutcomeManagementPanel',
@ -42,9 +43,13 @@ const Template = args => {
response[0].result.data = args.response
}
return (
<MockedProvider mocks={response} cache={createCache()}>
<OutcomeManagementPanel contextType={args.contextType} contextId={args.contextId} />
</MockedProvider>
<OutcomesContext.Provider
value={{env: {contextType: args.contextType, contextId: args.contextId}}}
>
<MockedProvider mocks={response} cache={createCache()}>
<OutcomeManagementPanel />
</MockedProvider>
</OutcomesContext.Provider>
)
}

View File

@ -24,7 +24,7 @@ import {Text} from '@instructure/ui-text'
import {Button} from '@instructure/ui-buttons'
import {View} from '@instructure/ui-view'
import Modal from '../../shared/components/InstuiModal'
import {useCanvasContext} from '../shared/hooks'
import useCanvasContext from '../shared/hooks/useCanvasContext'
import {showFlashAlert} from '../../shared/FlashAlert'
import {removeOutcome} from './api'

View File

@ -30,7 +30,7 @@ import ManageOutcomesFooter from './ManageOutcomesFooter'
import useSearch from 'jsx/shared/hooks/useSearch'
import TreeBrowser from './TreeBrowser'
import {useManageOutcomes} from 'jsx/outcomes/shared/treeBrowser'
import {useCanvasContext} from 'jsx/outcomes/shared/hooks'
import useCanvasContext from 'jsx/outcomes/shared/hooks/useCanvasContext'
import useModal from '../../shared/hooks/useModal'
import useGroupDetail from '../shared/hooks/useGroupDetail'
import MoveModal from './MoveModal'
@ -38,7 +38,8 @@ import GroupRemoveModal from './GroupRemoveModal'
import OutcomeRemoveModal from './OutcomeRemoveModal'
import useModalWithData from 'jsx/outcomes/shared/hooks/useModalWithData'
const NoOutcomesBillboard = ({contextType}) => {
const NoOutcomesBillboard = () => {
const {contextType} = useCanvasContext()
const isCourse = contextType === 'Course'
return (
@ -89,6 +90,7 @@ const OutcomeManagementPanel = () => {
selectedGroupId
} = useManageOutcomes()
const {loading, group, loadMore} = useGroupDetail(selectedGroupId)
const [isMoveGroupModalOpen, openMoveGroupModal, closeMoveGroupModal] = useModal()
const [isGroupRemoveModalOpen, openGroupRemoveModal, closeGroupRemoveModal] = useModal()
const [
@ -133,7 +135,7 @@ const OutcomeManagementPanel = () => {
return (
<div className="management-panel" data-testid="outcomeManagementPanel">
{!hasOutcomes ? (
<NoOutcomesBillboard contextType={contextType} />
<NoOutcomesBillboard />
) : (
<>
<Flex>

View File

@ -33,6 +33,7 @@ import {NumberInput} from '@instructure/ui-number-input'
import {SimpleSelect} from '@instructure/ui-simple-select'
import CalculationMethodContent from 'compiled/models/grade_summary/CalculationMethodContent'
import ConfirmMasteryModal from 'jsx/outcomes/ConfirmMasteryModal'
import useCanvasContext from 'jsx/outcomes/shared/hooks/useCanvasContext'
const validInt = (method, value) => {
if (method.validRange) {
@ -185,9 +186,9 @@ const ProficiencyCalculation = ({
update,
updateError,
canManage,
contextType,
onNotifyPendingChanges
}) => {
const {contextType} = useCanvasContext()
const {calculationMethod: initialMethodKey, calculationInt: initialInt} = method
const [calculationMethodKey, setCalculationMethodKey] = useState(initialMethodKey)
@ -299,8 +300,7 @@ ProficiencyCalculation.propTypes = {
canManage: PropTypes.bool,
update: PropTypes.func.isRequired,
onNotifyPendingChanges: PropTypes.func,
updateError: PropTypes.string,
contextType: PropTypes.string.isRequired
updateError: PropTypes.string
}
ProficiencyCalculation.defaultProps = {

View File

@ -17,7 +17,8 @@
*/
import React from 'react'
import {render, fireEvent, waitFor} from '@testing-library/react'
import {render as rtlRender, fireEvent, waitFor} from '@testing-library/react'
import OutcomesContext from '../../contexts/OutcomesContext'
import ProficiencyCalculation from '../ProficiencyCalculation'
describe('ProficiencyCalculation', () => {
@ -41,10 +42,17 @@ describe('ProficiencyCalculation', () => {
global.onerror = originalOnError
})
const render = (children, {contextType = 'Account', contextId = '1'} = {}) => {
return rtlRender(
<OutcomesContext.Provider value={{env: {contextType, contextId}}}>
{children}
</OutcomesContext.Provider>
)
}
const makeProps = (overrides = {}) => ({
update: Function.prototype,
canManage: true,
contextType: 'Account',
...overrides,
method: {
calculationMethod: 'decaying_average',
@ -335,9 +343,9 @@ describe('ProficiencyCalculation', () => {
})
it('renders correct text for the Course context', () => {
const {getByDisplayValue, getByText} = render(
<ProficiencyCalculation {...makeProps()} contextType="Course" />
)
const {getByDisplayValue, getByText} = render(<ProficiencyCalculation {...makeProps()} />, {
contextType: 'Course'
})
const method = getByDisplayValue('Decaying Average')
fireEvent.click(method)
const newMethod = getByText('Most Recent Score')

View File

@ -17,11 +17,14 @@
*/
import React from 'react'
import {render, waitFor, fireEvent, waitForElementToBeRemoved} from '@testing-library/react'
import {act, render as rtlRender, waitFor, fireEvent} from '@testing-library/react'
import {MockedProvider} from '@apollo/react-testing'
import OutcomesContext from '../../contexts/OutcomesContext'
import {ACCOUNT_OUTCOME_CALCULATION_QUERY, SET_OUTCOME_CALCULATION_METHOD} from '../api'
import MasteryCalculation from '../index'
import {masteryCalculationGraphqlMocks as mocks} from '../../__tests__/mocks'
import {masteryCalculationGraphqlMocks} from '../../__tests__/mocks'
jest.useFakeTimers()
describe('MasteryCalculation', () => {
beforeEach(() => {
@ -52,35 +55,37 @@ describe('MasteryCalculation', () => {
window.ENV = null
})
it('loads proficiency data', async () => {
const {getByText, queryByText, getByDisplayValue} = render(
<MockedProvider mocks={mocks}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
const render = (
children,
{contextType = 'Account', contextId = '11', mocks = masteryCalculationGraphqlMocks} = {}
) => {
return rtlRender(
<OutcomesContext.Provider value={{env: {contextType, contextId}}}>
<MockedProvider addTypename={false} mocks={mocks}>
{children}
</MockedProvider>
</OutcomesContext.Provider>
)
expect(getByText('Loading')).not.toEqual(null)
await waitForElementToBeRemoved(() => queryByText('Loading'))
}
it('loads proficiency data for Account', async () => {
const {getByDisplayValue} = render(<MasteryCalculation />)
await act(async () => jest.runAllTimers())
expect(getByDisplayValue(/65/)).not.toEqual(null)
})
it('loads calculation data for Course', async () => {
const {getByText, getByDisplayValue} = render(
<MockedProvider mocks={mocks}>
<MasteryCalculation contextType="Course" contextId="12" />
</MockedProvider>
)
expect(getByText('Loading')).not.toEqual(null)
await waitFor(() => expect(getByDisplayValue(/65/)).not.toEqual(null))
const {getByDisplayValue} = render(<MasteryCalculation />, {
contextType: 'Course',
contextId: '12'
})
await act(async () => jest.runAllTimers())
expect(getByDisplayValue(/65/)).not.toEqual(null)
})
it('loads role list', async () => {
const {getByText, queryByText, getAllByText} = render(
<MockedProvider mocks={mocks}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
expect(getByText('Loading')).not.toEqual(null)
await waitForElementToBeRemoved(() => queryByText('Loading'))
const {getByText, getAllByText} = render(<MasteryCalculation />)
await act(async () => jest.runAllTimers())
expect(
getByText(/Permission to change this mastery calculation at the account level is enabled for/)
).not.toEqual(null)
@ -92,12 +97,8 @@ describe('MasteryCalculation', () => {
})
it('displays an error on failed request', async () => {
const {getByText, queryByText} = render(
<MockedProvider mocks={[]}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
await waitForElementToBeRemoved(() => queryByText('Loading'))
const {getByText} = render(<MasteryCalculation />, {mocks: []})
await act(async () => jest.runAllTimers())
expect(getByText(/An error occurred/)).not.toEqual(null)
})
@ -120,12 +121,8 @@ describe('MasteryCalculation', () => {
}
}
]
const {getByText, queryByText} = render(
<MockedProvider mocks={emptyMocks}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
await waitForElementToBeRemoved(() => queryByText('Loading'))
const {getByText} = render(<MasteryCalculation />, {mocks: emptyMocks})
await act(async () => jest.runAllTimers())
expect(getByText('Mastery Calculation')).not.toBeNull()
})
@ -149,7 +146,7 @@ describe('MasteryCalculation', () => {
}
}))
const updateMocks = [
...mocks,
...masteryCalculationGraphqlMocks,
{
request: {
query: SET_OUTCOME_CALCULATION_METHOD,
@ -159,11 +156,8 @@ describe('MasteryCalculation', () => {
}
]
it('submits a request when calculation method is saved', async () => {
const {getByText, findByLabelText} = render(
<MockedProvider mocks={updateMocks} addTypename={false}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
const {getByText, findByLabelText} = render(<MasteryCalculation />, {mocks: updateMocks})
await act(async () => jest.runAllTimers())
const parameter = await findByLabelText(/Parameter/)
fireEvent.input(parameter, {target: {value: '88'}})
fireEvent.click(getByText('Save Mastery Calculation'))

View File

@ -29,8 +29,10 @@ import {
SET_OUTCOME_CALCULATION_METHOD
} from './api'
import {useQuery, useMutation} from 'react-apollo'
import useCanvasContext from 'jsx/outcomes/shared/hooks/useCanvasContext'
const MasteryCalculation = ({contextType, contextId, onNotifyPendingChanges}) => {
const MasteryCalculation = ({onNotifyPendingChanges}) => {
const {contextType, contextId} = useCanvasContext()
const query =
contextType === 'Course' ? COURSE_OUTCOME_CALCULATION_QUERY : ACCOUNT_OUTCOME_CALCULATION_QUERY
const {loading, error, data} = useQuery(query, {
@ -71,8 +73,6 @@ const MasteryCalculation = ({contextType, contextId, onNotifyPendingChanges}) =>
return (
<>
<ProficiencyCalculation
contextType={contextType}
contextId={contextId}
method={outcomeCalculationMethod || undefined} // send undefined when value is null
update={setCalculationMethod}
updateError={setCalculationMethodError}

View File

@ -17,12 +17,15 @@
*/
import React from 'react'
import {render, waitFor, fireEvent} from '@testing-library/react'
import {render as rtlRender, waitFor, fireEvent} from '@testing-library/react'
import {MockedProvider} from '@apollo/react-testing'
import moxios from 'moxios'
import OutcomesContext from '../../contexts/OutcomesContext'
import {ACCOUNT_OUTCOME_PROFICIENCY_QUERY} from '../api'
import MasteryScale from '../index'
import {masteryScalesGraphqlMocks as mocks} from '../../__tests__/mocks'
import {masteryScalesGraphqlMocks} from '../../__tests__/mocks'
jest.useFakeTimers()
describe('MasteryScale', () => {
beforeEach(() => {
@ -53,32 +56,34 @@ describe('MasteryScale', () => {
window.ENV = null
})
it('loads proficiency data', async () => {
const {getByText, getByDisplayValue} = render(
<MockedProvider mocks={mocks}>
<MasteryScale contextType="Account" contextId="11" />
</MockedProvider>
const render = (
children,
{contextType = 'Account', contextId = '11', mocks = masteryScalesGraphqlMocks} = {}
) => {
return rtlRender(
<OutcomesContext.Provider value={{env: {contextType, contextId}}}>
<MockedProvider mocks={mocks}>{children}</MockedProvider>
</OutcomesContext.Provider>
)
}
it('loads proficiency data', async () => {
const {getByText, getByDisplayValue} = render(<MasteryScale />)
expect(getByText('Loading')).toBeInTheDocument()
await waitFor(() => expect(getByDisplayValue(/Rating A/)).toBeInTheDocument())
})
it('loads proficiency data to Course', async () => {
const {getByText, getByDisplayValue} = render(
<MockedProvider mocks={mocks}>
<MasteryScale contextType="Course" contextId="12" />
</MockedProvider>
<MasteryScale contextType="Course" contextId="12" />,
{contextType: 'Course', contextId: '12'}
)
expect(getByText('Loading')).toBeInTheDocument()
await waitFor(() => expect(getByDisplayValue(/Rating A/)).toBeInTheDocument())
})
it('loads role list', async () => {
const {getByText, getAllByText} = render(
<MockedProvider mocks={mocks}>
<MasteryScale contextType="Account" contextId="11" />
</MockedProvider>
)
const {getByText, getAllByText} = render(<MasteryScale />)
expect(getByText('Loading')).toBeInTheDocument()
await waitFor(() => {
expect(
@ -93,11 +98,7 @@ describe('MasteryScale', () => {
})
it('displays an error on failed request', async () => {
const {getByText} = render(
<MockedProvider mocks={[]}>
<MasteryScale contextType="Account" contextId="11" />
</MockedProvider>
)
const {getByText} = render(<MasteryScale />, {mocks: []})
await waitFor(() => expect(getByText(/An error occurred/)).toBeInTheDocument())
})
@ -120,11 +121,7 @@ describe('MasteryScale', () => {
}
}
]
const {getByText} = render(
<MockedProvider mocks={emptyMocks}>
<MasteryScale contextType="Account" contextId="11" />
</MockedProvider>
)
const {getByText} = render(<MasteryScale />, {mocks: emptyMocks})
await waitFor(() => expect(getByText('Mastery')).not.toBeNull())
})
@ -137,11 +134,7 @@ describe('MasteryScale', () => {
})
it('submits a request when ratings are saved', async () => {
const {findAllByLabelText, getByText} = render(
<MockedProvider mocks={mocks}>
<MasteryScale contextType="Account" contextId="11" />
</MockedProvider>
)
const {findAllByLabelText, getByText} = render(<MasteryScale />)
const pointsInput = (await findAllByLabelText(/Change points/))[0]
fireEvent.change(pointsInput, {target: {value: '100'}})
fireEvent.click(getByText('Save Mastery Scale'))
@ -167,11 +160,7 @@ describe('MasteryScale', () => {
})
it('hides mastery info', async () => {
const {getByText, queryByText} = render(
<MockedProvider mocks={mocks}>
<MasteryScale contextType="Account" contextId="11" />
</MockedProvider>
)
const {getByText, queryByText} = render(<MasteryScale />)
expect(getByText('Loading')).toBeInTheDocument()
await waitFor(() =>
expect(

View File

@ -17,6 +17,7 @@
*/
import React, {useCallback, useState} from 'react'
import {useQuery} from 'react-apollo'
import I18n from 'i18n!MasteryScale'
import {Spinner} from '@instructure/ui-spinner'
import {Text} from '@instructure/ui-text'
@ -27,9 +28,10 @@ import {
ACCOUNT_OUTCOME_PROFICIENCY_QUERY,
COURSE_OUTCOME_PROFICIENCY_QUERY
} from './api'
import {useQuery} from 'react-apollo'
import useCanvasContext from 'jsx/outcomes/shared/hooks/useCanvasContext'
const MasteryScale = ({contextType, contextId, onNotifyPendingChanges}) => {
const MasteryScale = ({onNotifyPendingChanges}) => {
const {contextType, contextId} = useCanvasContext()
const query =
contextType === 'Course' ? COURSE_OUTCOME_PROFICIENCY_QUERY : ACCOUNT_OUTCOME_PROFICIENCY_QUERY

View File

@ -21,7 +21,7 @@ import {Tabs} from '@instructure/ui-tabs'
import MasteryScale from 'jsx/outcomes/MasteryScale'
import MasteryCalculation from 'jsx/outcomes/MasteryCalculation'
import {ApolloProvider, createClient} from 'jsx/canvas-apollo'
import OutcomesContext from './contexts/OutcomesContext'
import OutcomesContext, {getContext} from './contexts/OutcomesContext'
import ManagementHeader from './ManagementHeader'
import OutcomeManagementPanel from './Management'
@ -88,35 +88,18 @@ export const OutcomeManagementWithoutGraphql = () => {
}
}, [hasUnsavedChangesRef])
const [snakeContextType, contextId] = ENV.context_asset_string.split('_')
const contextType = snakeContextType === 'course' ? 'Course' : 'Account'
const contextStore = {
env: {
contextType,
contextId
}
}
return (
<OutcomesContext.Provider value={contextStore}>
<OutcomesContext.Provider value={getContext()}>
{improvedManagement && <ManagementHeader />}
<Tabs onRequestTabChange={handleTabChange}>
<Tabs.Panel renderTitle={I18n.t('Manage')} isSelected={selectedIndex === 0}>
{improvedManagement ? <OutcomeManagementPanel /> : <OutcomePanel />}
</Tabs.Panel>
<Tabs.Panel renderTitle={I18n.t('Mastery')} isSelected={selectedIndex === 1}>
<MasteryScale
onNotifyPendingChanges={setHasUnsavedChanges}
contextType={contextType}
contextId={contextId}
/>
<MasteryScale onNotifyPendingChanges={setHasUnsavedChanges} />
</Tabs.Panel>
<Tabs.Panel renderTitle={I18n.t('Calculation')} isSelected={selectedIndex === 2}>
<MasteryCalculation
onNotifyPendingChanges={setHasUnsavedChanges}
contextType={contextType}
contextId={contextId}
/>
<MasteryCalculation onNotifyPendingChanges={setHasUnsavedChanges} />
</Tabs.Panel>
</Tabs>
</OutcomesContext.Provider>

View File

@ -19,4 +19,16 @@
import {createContext} from 'react'
const OutcomesContext = createContext({})
export const getContext = () => {
const [snakeContextType, contextId] = ENV.context_asset_string.split('_')
const contextType = snakeContextType === 'course' ? 'Course' : 'Account'
return {
env: {
contextType,
contextId
}
}
}
export default OutcomesContext

View File

@ -19,7 +19,7 @@
import React from 'react'
import {renderHook} from '@testing-library/react-hooks/dom'
import OutcomesContext from 'jsx/outcomes/contexts/OutcomesContext'
import {useCanvasContext} from '../hooks'
import useCanvasContext from '../useCanvasContext'
describe('useCanvasContext', () => {
test('can return values if they are set', () => {

View File

@ -17,9 +17,9 @@
*/
import {useContext} from 'react'
import OutcomesContext from '../contexts/OutcomesContext'
import OutcomesContext from '../../contexts/OutcomesContext'
export const useCanvasContext = () => {
const useCanvasContext = () => {
const context = useContext(OutcomesContext)
const contextType = context?.env?.contextType
const contextId = context?.env?.contextId
@ -28,3 +28,5 @@ export const useCanvasContext = () => {
contextId
}
}
export default useCanvasContext

View File

@ -22,8 +22,8 @@ import I18n from 'i18n!OutcomeManagement'
import 'compiled/jquery.rails_flash_notifications'
import {CHILD_GROUPS_QUERY} from '../Management/api'
import {FIND_GROUPS_QUERY} from '../api'
import {useCanvasContext} from './hooks'
import useSearch from '../../shared/hooks/useSearch'
import useCanvasContext from './hooks/useCanvasContext'
export const ROOT_ID = 0
export const ACCOUNT_FOLDER_ID = -1