Prevent 404 error in Direct Share tray
fixes LA-325 flag=direct_share Test plan - In the direct share import tray select a course and then a module for that course - Then search for a different course and select one from the searched list - Verify that you don't get the "something went wrong" picture Change-Id: I70b7614da7d9003c76bf3ebe3ff1290e4de98575 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/219207 Tested-by: Jenkins Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jon Willesen <jonw+gerrit@instructure.com> QA-Review: Carl Kibler <ckibler@instructure.com> Product-Review: Mysti Lilla <mysti@instructure.com>
This commit is contained in:
parent
0dd1a45d95
commit
3849cfad1c
|
@ -46,7 +46,7 @@ export default function CourseImportPanel({contentShare, onClose, onImport}) {
|
|||
migration_type: 'canvas_cartridge_importer',
|
||||
settings: {
|
||||
content_export_id: contentShare.content_export.id,
|
||||
insert_into_module_id: selectedModule?.id,
|
||||
insert_into_module_id: selectedModule?.id || null,
|
||||
insert_into_module_type: contentShare.content_type,
|
||||
insert_into_module_position: selectedPosition
|
||||
}
|
||||
|
@ -56,6 +56,11 @@ export default function CourseImportPanel({contentShare, onClose, onImport}) {
|
|||
onImport(contentShare)
|
||||
}
|
||||
|
||||
function handleSelectedCourse(course) {
|
||||
setSelectedModule(null)
|
||||
setSelectedCourse(course)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DirectShareOperationStatus
|
||||
|
@ -66,8 +71,8 @@ export default function CourseImportPanel({contentShare, onClose, onImport}) {
|
|||
/>
|
||||
<CourseAndModulePicker
|
||||
selectedCourseId={selectedCourse?.id}
|
||||
setSelectedCourse={setSelectedCourse}
|
||||
selectedModuleId={selectedModule?.id}
|
||||
setSelectedCourse={handleSelectedCourse}
|
||||
selectedModuleId={selectedModule?.id || null}
|
||||
setSelectedModule={setSelectedModule}
|
||||
setModuleItemPosition={setSelectedPosition}
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,9 @@ import React from 'react'
|
|||
import {render, fireEvent, act} from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import useManagedCourseSearchApi from 'jsx/shared/effects/useManagedCourseSearchApi'
|
||||
import useModuleCourseSearchApi from 'jsx/shared/effects/useModuleCourseSearchApi'
|
||||
import useModuleCourseSearchApi, {
|
||||
useCourseModuleItemApi
|
||||
} from 'jsx/shared/effects/useModuleCourseSearchApi'
|
||||
import CourseImportPanel from '../CourseImportPanel'
|
||||
import {mockShare} from 'jsx/content_shares/__tests__/test-utils'
|
||||
|
||||
|
@ -43,7 +45,10 @@ describe('CourseImportPanel', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
useManagedCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', name: 'abc'}, {id: 'cde', name: 'cde'}])
|
||||
success([
|
||||
{id: 'abc', name: 'abc'},
|
||||
{id: 'cde', name: 'cde'}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -93,7 +98,10 @@ describe('CourseImportPanel', () => {
|
|||
workflow_state: 'running'
|
||||
})
|
||||
useModuleCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: '1', name: 'Module 1'}, {id: '2', name: 'Module 2'}])
|
||||
success([
|
||||
{id: '1', name: 'Module 1'},
|
||||
{id: '2', name: 'Module 2'}
|
||||
])
|
||||
})
|
||||
const {getByText, getByLabelText, queryByText} = render(
|
||||
<CourseImportPanel contentShare={share} onImport={onImport} />
|
||||
|
@ -121,6 +129,34 @@ describe('CourseImportPanel', () => {
|
|||
expect(onImport.mock.calls[0][0]).toBe(share)
|
||||
})
|
||||
|
||||
it('deletes the module and removes the position selector when a new course is selected', () => {
|
||||
const share = mockShare()
|
||||
useModuleCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([
|
||||
{id: '1', name: 'Module 1'},
|
||||
{id: '2', name: 'Module 2'}
|
||||
])
|
||||
})
|
||||
const {getByText, getByLabelText, queryByText} = render(
|
||||
<CourseImportPanel contentShare={share} />
|
||||
)
|
||||
const courseSelector = getByText(/select a course/i)
|
||||
fireEvent.click(courseSelector)
|
||||
fireEvent.click(getByText('abc'))
|
||||
fireEvent.click(getByText(/select a module/i))
|
||||
fireEvent.click(getByText(/Module 1/))
|
||||
expect(getByText(/Position/)).toBeInTheDocument()
|
||||
useManagedCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'ghi', name: 'foo'}])
|
||||
})
|
||||
useCourseModuleItemApi.mockClear()
|
||||
const input = getByLabelText(/select a course/i)
|
||||
fireEvent.change(input, {target: {value: 'fo'}})
|
||||
fireEvent.click(getByText('foo'))
|
||||
expect(queryByText(/Position/)).not.toBeInTheDocument()
|
||||
expect(useCourseModuleItemApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation()
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react'
|
||||
import {bool, string, func} from 'prop-types'
|
||||
import {Button} from '@instructure/ui-buttons'
|
||||
import {Flex} from '@instructure/ui-layout'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
|
||||
ConfirmActionButtonBar.propTypes = {
|
||||
// only buttons with labels will be displayed
|
||||
|
|
|
@ -52,25 +52,25 @@ export default function CourseAndModulePicker({
|
|||
itemSearchFunction={useManagedCourseSearchApi}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item padding="small">
|
||||
{selectedCourseId && (
|
||||
{selectedCourseId && (
|
||||
<Flex.Item padding="small">
|
||||
<SearchItemSelector
|
||||
onItemSelected={setSelectedModule}
|
||||
renderLabel={I18n.t('Select a Module (optional)')}
|
||||
itemSearchFunction={useModuleCourseSearchApi}
|
||||
contextId={selectedCourseId}
|
||||
contextId={selectedCourseId || null}
|
||||
/>
|
||||
)}
|
||||
</Flex.Item>
|
||||
<Flex.Item padding="small">
|
||||
{selectedCourseId && selectedModuleId && (
|
||||
</Flex.Item>
|
||||
)}
|
||||
{selectedCourseId && selectedModuleId && (
|
||||
<Flex.Item padding="small">
|
||||
<ModulePositionPicker
|
||||
courseId={selectedCourseId}
|
||||
moduleId={selectedModuleId}
|
||||
courseId={selectedCourseId || null}
|
||||
moduleId={selectedModuleId || null}
|
||||
setModuleItemPosition={setModuleItemPosition}
|
||||
/>
|
||||
)}
|
||||
</Flex.Item>
|
||||
</Flex.Item>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -52,7 +52,7 @@ export default function DirectShareCoursePanel({sourceCourseId, contentSelection
|
|||
select: contentSelection,
|
||||
settings: {
|
||||
source_course_id: sourceCourseId,
|
||||
insert_into_module_id: selectedModule?.id,
|
||||
insert_into_module_id: selectedModule?.id || null,
|
||||
insert_into_module_type: contentSelection ? Object.keys(contentSelection)[0] : null,
|
||||
insert_into_module_position: selectedPosition
|
||||
}
|
||||
|
@ -61,6 +61,11 @@ export default function DirectShareCoursePanel({sourceCourseId, contentSelection
|
|||
)
|
||||
}
|
||||
|
||||
function handleSelectedCourse(course) {
|
||||
setSelectedModule(null)
|
||||
setSelectedCourse(course)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DirectShareOperationStatus
|
||||
|
@ -71,8 +76,8 @@ export default function DirectShareCoursePanel({sourceCourseId, contentSelection
|
|||
/>
|
||||
<CourseAndModulePicker
|
||||
selectedCourseId={selectedCourse?.id}
|
||||
setSelectedCourse={setSelectedCourse}
|
||||
selectedModuleId={selectedModule?.id}
|
||||
setSelectedCourse={handleSelectedCourse}
|
||||
selectedModuleId={selectedModule?.id || null}
|
||||
setSelectedModule={setSelectedModule}
|
||||
setModuleItemPosition={setSelectedPosition}
|
||||
/>
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react'
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
import useManagedCourseSearchApi from 'jsx/shared/effects/useManagedCourseSearchApi'
|
||||
import useModuleCourseSearchApi from 'jsx/shared/effects/useModuleCourseSearchApi'
|
||||
import useModuleCourseSearchApi, {
|
||||
useCourseModuleItemApi
|
||||
} from 'jsx/shared/effects/useModuleCourseSearchApi'
|
||||
import CourseAndModulePicker from '../CourseAndModulePicker'
|
||||
|
||||
jest.mock('jsx/shared/effects/useManagedCourseSearchApi')
|
||||
|
@ -39,15 +41,73 @@ describe('CourseAndModulePicker', () => {
|
|||
if (ariaLive) ariaLive.remove()
|
||||
})
|
||||
|
||||
it('enables the module selector when a course is selected', () => {
|
||||
it('shows course selector by default', () => {
|
||||
useManagedCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', name: 'abc'}, {id: 'cde', name: 'cde'}])
|
||||
success([
|
||||
{id: 'abc', name: 'abc'},
|
||||
{id: 'cde', name: 'cde'}
|
||||
])
|
||||
})
|
||||
const setCourse = jest.fn()
|
||||
const {getByText} = render(<CourseAndModulePicker setSelectedCourse={setCourse} />)
|
||||
const selector = getByText(/select a course/i)
|
||||
fireEvent.click(selector)
|
||||
fireEvent.click(getByText('abc'))
|
||||
expect(setCourse).toHaveBeenLastCalledWith({id: 'abc', name: 'abc'})
|
||||
})
|
||||
|
||||
it('shows the course and module selector when a course is given', () => {
|
||||
useManagedCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([
|
||||
{id: 'abc', name: 'abc'},
|
||||
{id: 'cde', name: 'cde'}
|
||||
])
|
||||
})
|
||||
useModuleCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: '1', name: 'Module 1'}, {id: '2', name: 'Module 2'}])
|
||||
success([
|
||||
{id: '1', name: 'Module 1'},
|
||||
{id: '2', name: 'Module 2'}
|
||||
])
|
||||
})
|
||||
const {getByText} = render(<CourseAndModulePicker selectedCourseId="abc" />)
|
||||
expect(getByText(/select a course/i)).toBeInTheDocument()
|
||||
expect(getByText(/select a module/i)).toBeInTheDocument()
|
||||
const setModule = jest.fn()
|
||||
const {getByText} = render(
|
||||
<CourseAndModulePicker selectedCourseId="abc" setSelectedModule={setModule} />
|
||||
)
|
||||
const selector = getByText(/select a module/i)
|
||||
fireEvent.click(selector)
|
||||
fireEvent.click(getByText('Module 1'))
|
||||
expect(setModule).toHaveBeenLastCalledWith({id: '1', name: 'Module 1'})
|
||||
})
|
||||
|
||||
it('shows the position selector when a module is given', () => {
|
||||
useManagedCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([
|
||||
{id: 'abc', name: 'abc'},
|
||||
{id: 'cde', name: 'cde'}
|
||||
])
|
||||
})
|
||||
useModuleCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([
|
||||
{id: '1', name: 'Module 1'},
|
||||
{id: '2', name: 'Module 2'}
|
||||
])
|
||||
})
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([
|
||||
{id: 'a', title: 'Item 1', position: '5'},
|
||||
{id: 'b', title: 'Item 2', position: '6'}
|
||||
])
|
||||
})
|
||||
const setPosition = jest.fn()
|
||||
const {getByTestId} = render(
|
||||
<CourseAndModulePicker
|
||||
selectedCourseId="abc"
|
||||
selectedModuleId="1"
|
||||
setModuleItemPosition={setPosition}
|
||||
/>
|
||||
)
|
||||
const selector = getByTestId('select-position')
|
||||
fireEvent.change(selector, {target: {value: 'top'}})
|
||||
expect(setPosition).toHaveBeenLastCalledWith(1)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -20,9 +20,13 @@ import React from 'react'
|
|||
import {render, fireEvent, act} from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import useManagedCourseSearchApi from 'jsx/shared/effects/useManagedCourseSearchApi'
|
||||
import useModuleCourseSearchApi, {
|
||||
useCourseModuleItemApi
|
||||
} from 'jsx/shared/effects/useModuleCourseSearchApi'
|
||||
import DirectShareCoursePanel from '../DirectShareCoursePanel'
|
||||
|
||||
jest.mock('jsx/shared/effects/useManagedCourseSearchApi')
|
||||
jest.mock('jsx/shared/effects/useModuleCourseSearchApi')
|
||||
|
||||
describe('DirectShareCoursePanel', () => {
|
||||
let ariaLive
|
||||
|
@ -40,7 +44,10 @@ describe('DirectShareCoursePanel', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
useManagedCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', name: 'abc'}, {id: 'cde', name: 'cde'}])
|
||||
success([
|
||||
{id: 'abc', name: 'abc'},
|
||||
{id: 'cde', name: 'cde'}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -121,6 +128,36 @@ describe('DirectShareCoursePanel', () => {
|
|||
expect(getByText('Close')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('deletes the module and removes the position selector when a new course is selected', () => {
|
||||
useModuleCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([
|
||||
{id: '1', name: 'Module 1'},
|
||||
{id: '2', name: 'Module 2'}
|
||||
])
|
||||
})
|
||||
const {getByText, getByLabelText, queryByText} = render(
|
||||
<DirectShareCoursePanel
|
||||
sourceCourseId="42"
|
||||
contentSelection={{discussion_topics: ['1123']}}
|
||||
/>
|
||||
)
|
||||
const courseSelector = getByText(/select a course/i)
|
||||
fireEvent.click(courseSelector)
|
||||
fireEvent.click(getByText('abc'))
|
||||
fireEvent.click(getByText(/select a module/i))
|
||||
fireEvent.click(getByText(/Module 1/))
|
||||
expect(getByText(/Position/)).toBeInTheDocument()
|
||||
useManagedCourseSearchApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'ghi', name: 'foo'}])
|
||||
})
|
||||
useCourseModuleItemApi.mockClear()
|
||||
const input = getByLabelText(/select a course/i)
|
||||
fireEvent.change(input, {target: {value: 'fo'}})
|
||||
fireEvent.click(getByText('foo'))
|
||||
expect(queryByText(/Position/)).not.toBeInTheDocument()
|
||||
expect(useCourseModuleItemApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation()
|
||||
|
|
|
@ -20,10 +20,10 @@ import React from 'react'
|
|||
import {string, func, bool, arrayOf, node, shape} from 'prop-types'
|
||||
import I18n from 'i18n!selectPosition'
|
||||
import ConnectorIcon from '../../move_item/ConnectorIcon'
|
||||
import {Text} from '@instructure/ui-elements'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {FormField} from '@instructure/ui-form-field'
|
||||
import {View} from '@instructure/ui-layout'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {positions} from '../../move_item/positions'
|
||||
import {itemShape} from '../../move_item/propTypes'
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"@instructure/ui-svg-images": "6",
|
||||
"@instructure/ui-table": "6",
|
||||
"@instructure/ui-tabs": "6",
|
||||
"@instructure/ui-text": "6",
|
||||
"@instructure/ui-text-input": "6",
|
||||
"@instructure/ui-themeable": "6",
|
||||
"@instructure/ui-themes": "6",
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -2331,6 +2331,18 @@
|
|||
classnames "^2"
|
||||
prop-types "^15"
|
||||
|
||||
"@instructure/ui-text@6":
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@instructure/ui-text/-/ui-text-6.15.0.tgz#0bceef3fb9a2b5640b54d5f00664a37cee4ce4ba"
|
||||
integrity sha512-KXF73rRC/RkIcryWnHGNnQJi3JcGcGWjJLm0kaDljXCQBS25ypvURg9OCiSYhvyInFB7fLQFP1anAAY2UyPISQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.0"
|
||||
"@instructure/console" "^6.15.0"
|
||||
"@instructure/ui-react-utils" "^6.15.0"
|
||||
"@instructure/ui-themeable" "^6.15.0"
|
||||
classnames "^2"
|
||||
prop-types "^15"
|
||||
|
||||
"@instructure/ui-themeable@6", "@instructure/ui-themeable@^6.15.0", "@instructure/ui-themeable@^6.9.0":
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@instructure/ui-themeable/-/ui-themeable-6.15.0.tgz#8ef3f08020731385d9df5d0213c10692e3a1cbd3"
|
||||
|
|
Loading…
Reference in New Issue