Add apps to k5 course resources tab
This loads the non-hidden, course nav apps for the k5 subject course on the resources tab. closes LS-2062 flag=canvas_for_elementary Test plan: - Open up a k5 subject course as a teacher or student - Click the resources tab - Expect to see whatever apps appear for that user on the classic course nav - Click an app, expect app to open in course context - Reload page, expect to see loading spinner briefly - Simulate network failure, expect to see error message when loading apps Change-Id: Ie483315d02feaeaeea835656dacf2739902ee2ef Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/262572 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jonathan Guardado <jonathan.guardado@instructure.com> QA-Review: Jonathan Guardado <jonathan.guardado@instructure.com> Product-Review: Peyton Craighill <pcraighill@instructure.com>
This commit is contained in:
parent
86c6f99285
commit
e1ccc28388
|
@ -30,6 +30,7 @@ ready(() => {
|
||||||
<K5Course
|
<K5Course
|
||||||
currentUser={ENV.current_user}
|
currentUser={ENV.current_user}
|
||||||
name={ENV.COURSE.name}
|
name={ENV.COURSE.name}
|
||||||
|
id={ENV.COURSE.id}
|
||||||
imageUrl={ENV.COURSE.image_url}
|
imageUrl={ENV.COURSE.image_url}
|
||||||
plannerEnabled={ENV.STUDENT_PLANNER_ENABLED}
|
plannerEnabled={ENV.STUDENT_PLANNER_ENABLED}
|
||||||
timeZone={ENV.TIMEZONE}
|
timeZone={ENV.TIMEZONE}
|
||||||
|
|
|
@ -29,7 +29,8 @@ import {
|
||||||
IconCalendarMonthLine,
|
IconCalendarMonthLine,
|
||||||
IconHomeLine,
|
IconHomeLine,
|
||||||
IconModuleLine,
|
IconModuleLine,
|
||||||
IconStarLightLine
|
IconStarLightLine,
|
||||||
|
IconBankLine
|
||||||
} from '@instructure/ui-icons'
|
} from '@instructure/ui-icons'
|
||||||
import {ApplyTheme} from '@instructure/ui-themeable'
|
import {ApplyTheme} from '@instructure/ui-themeable'
|
||||||
import {Heading} from '@instructure/ui-heading'
|
import {Heading} from '@instructure/ui-heading'
|
||||||
|
@ -42,8 +43,10 @@ import SchedulePage from '@canvas/k5/react/SchedulePage'
|
||||||
import usePlanner from '@canvas/k5/react/hooks/usePlanner'
|
import usePlanner from '@canvas/k5/react/hooks/usePlanner'
|
||||||
import useTabState from '@canvas/k5/react/hooks/useTabState'
|
import useTabState from '@canvas/k5/react/hooks/useTabState'
|
||||||
import {mapStateToProps} from '@canvas/k5/redux/redux-helpers'
|
import {mapStateToProps} from '@canvas/k5/redux/redux-helpers'
|
||||||
import {TAB_IDS} from '@canvas/k5/react/utils'
|
import {TAB_IDS, fetchCourseApps} from '@canvas/k5/react/utils'
|
||||||
import k5Theme, {theme} from '@canvas/k5/react/k5-theme'
|
import k5Theme, {theme} from '@canvas/k5/react/k5-theme'
|
||||||
|
import AppsList from '@canvas/k5/react/AppsList'
|
||||||
|
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||||
|
|
||||||
const DEFAULT_COLOR = k5Theme.variables.colors.backgroundMedium
|
const DEFAULT_COLOR = k5Theme.variables.colors.backgroundMedium
|
||||||
const HERO_HEIGHT_PX = 400
|
const HERO_HEIGHT_PX = 400
|
||||||
|
@ -68,6 +71,11 @@ const COURSE_TABS = [
|
||||||
id: TAB_IDS.GRADES,
|
id: TAB_IDS.GRADES,
|
||||||
icon: IconStarLightLine,
|
icon: IconStarLightLine,
|
||||||
label: I18n.t('Grades')
|
label: I18n.t('Grades')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TAB_IDS.RESOURCES,
|
||||||
|
icon: IconBankLine,
|
||||||
|
label: I18n.t('Resources')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -108,6 +116,16 @@ export function CourseHeaderHero({name, image, backgroundColor}) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchApps = (courseId, courseName) =>
|
||||||
|
fetchCourseApps(courseId).then(apps =>
|
||||||
|
apps.map(app => ({
|
||||||
|
id: app.id,
|
||||||
|
courses: [{id: courseId, name: courseName}],
|
||||||
|
title: app.course_navigation.text || app.name,
|
||||||
|
icon: app.course_navigation.icon_url || app.icon_url
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
export function K5Course({
|
export function K5Course({
|
||||||
assignmentsDueToday,
|
assignmentsDueToday,
|
||||||
assignmentsMissing,
|
assignmentsMissing,
|
||||||
|
@ -115,6 +133,7 @@ export function K5Course({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
loadAllOpportunities,
|
loadAllOpportunities,
|
||||||
name,
|
name,
|
||||||
|
id,
|
||||||
timeZone,
|
timeZone,
|
||||||
defaultTab = TAB_IDS.OVERVIEW,
|
defaultTab = TAB_IDS.OVERVIEW,
|
||||||
plannerEnabled = false
|
plannerEnabled = false
|
||||||
|
@ -128,6 +147,8 @@ export function K5Course({
|
||||||
callback: () => loadAllOpportunities(),
|
callback: () => loadAllOpportunities(),
|
||||||
singleCourse: true
|
singleCourse: true
|
||||||
})
|
})
|
||||||
|
const [apps, setApps] = useState([])
|
||||||
|
const [isAppsLoading, setAppsLoading] = useState(false)
|
||||||
|
|
||||||
const modulesRef = useRef(null)
|
const modulesRef = useRef(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -140,6 +161,14 @@ export function K5Course({
|
||||||
}
|
}
|
||||||
}, [currentTab])
|
}, [currentTab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAppsLoading(true)
|
||||||
|
fetchApps(id, name)
|
||||||
|
.then(setApps)
|
||||||
|
.catch(showFlashError(I18n.t('Failed to load apps for %{name}.', {name})))
|
||||||
|
.finally(() => setAppsLoading(false))
|
||||||
|
}, [id, name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<K5DashboardContext.Provider
|
<K5DashboardContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -160,6 +189,7 @@ export function K5Course({
|
||||||
</K5Tabs>
|
</K5Tabs>
|
||||||
{plannerInitialized && <SchedulePage visible={currentTab === TAB_IDS.SCHEDULE} />}
|
{plannerInitialized && <SchedulePage visible={currentTab === TAB_IDS.SCHEDULE} />}
|
||||||
{!plannerEnabled && currentTab === TAB_IDS.SCHEDULE && createTeacherPreview(timeZone)}
|
{!plannerEnabled && currentTab === TAB_IDS.SCHEDULE && createTeacherPreview(timeZone)}
|
||||||
|
{currentTab === TAB_IDS.RESOURCES && <AppsList isLoading={isAppsLoading} apps={apps} />}
|
||||||
</View>
|
</View>
|
||||||
</K5DashboardContext.Provider>
|
</K5DashboardContext.Provider>
|
||||||
)
|
)
|
||||||
|
@ -171,6 +201,7 @@ K5Course.propTypes = {
|
||||||
assignmentsCompletedForToday: PropTypes.object.isRequired,
|
assignmentsCompletedForToday: PropTypes.object.isRequired,
|
||||||
loadAllOpportunities: PropTypes.func.isRequired,
|
loadAllOpportunities: PropTypes.func.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
timeZone: PropTypes.string.isRequired,
|
timeZone: PropTypes.string.isRequired,
|
||||||
defaultTab: PropTypes.string,
|
defaultTab: PropTypes.string,
|
||||||
imageUrl: PropTypes.string,
|
imageUrl: PropTypes.string,
|
||||||
|
|
|
@ -20,6 +20,8 @@ import React from 'react'
|
||||||
import moxios from 'moxios'
|
import moxios from 'moxios'
|
||||||
import {render, waitFor} from '@testing-library/react'
|
import {render, waitFor} from '@testing-library/react'
|
||||||
import {K5Course} from '../K5Course'
|
import {K5Course} from '../K5Course'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import {TAB_IDS} from '@canvas/k5/react/utils'
|
||||||
|
|
||||||
const currentUser = {
|
const currentUser = {
|
||||||
id: '1',
|
id: '1',
|
||||||
|
@ -42,15 +44,28 @@ const defaultProps = {
|
||||||
currentUser,
|
currentUser,
|
||||||
loadAllOpportunities: () => {},
|
loadAllOpportunities: () => {},
|
||||||
name: 'Arts and Crafts',
|
name: 'Arts and Crafts',
|
||||||
|
id: '30',
|
||||||
timeZone: defaultEnv.TIMEZONE
|
timeZone: defaultEnv.TIMEZONE
|
||||||
}
|
}
|
||||||
|
const fetchAppsResponse = [
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
course_navigation: {
|
||||||
|
text: 'Studio',
|
||||||
|
icon_url: 'studio.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const FETCH_APPS_URL = '/api/v1/courses/30/external_tools/visible_course_nav_tools'
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
moxios.install()
|
moxios.install()
|
||||||
|
fetchMock.get(FETCH_APPS_URL, JSON.stringify(fetchAppsResponse))
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
moxios.uninstall()
|
moxios.uninstall()
|
||||||
|
fetchMock.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -84,9 +99,9 @@ describe('K-5 Subject Course', () => {
|
||||||
expect(getByText(defaultProps.name)).toBeInTheDocument()
|
expect(getByText(defaultProps.name)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows Overview, Schedule, Modules, and Grades options', () => {
|
it('shows Overview, Schedule, Modules, Grades, and Resources options', () => {
|
||||||
const {getByText} = render(<K5Course {...defaultProps} />)
|
const {getByText} = render(<K5Course {...defaultProps} />)
|
||||||
;['Overview', 'Schedule', 'Modules', 'Grades'].forEach(label =>
|
;['Overview', 'Schedule', 'Modules', 'Grades', 'Resources'].forEach(label =>
|
||||||
expect(getByText(label)).toBeInTheDocument()
|
expect(getByText(label)).toBeInTheDocument()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -108,4 +123,30 @@ describe('K-5 Subject Course', () => {
|
||||||
waitFor(() => expect(modulesContainer.style.display).toBe('block'))
|
waitFor(() => expect(modulesContainer.style.display).toBe('block'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('resources tab', () => {
|
||||||
|
it("displays user's apps", () => {
|
||||||
|
const {getByText} = render(<K5Course {...defaultProps} defaultTab={TAB_IDS.RESOURCES} />)
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getByText('Studio')).toBeInTheDocument()
|
||||||
|
expect(getByText('Student Applications')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a loading spinner while apps are loading', () => {
|
||||||
|
const {getByText, queryByText} = render(
|
||||||
|
<K5Course {...defaultProps} defaultTab={TAB_IDS.RESOURCES} />
|
||||||
|
)
|
||||||
|
waitFor(() => expect(getByText('Loading apps...')).toBeInTheDocument())
|
||||||
|
expect(queryByText('Studio')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows an error if apps fail to load', () => {
|
||||||
|
fetchMock.get(FETCH_APPS_URL, 400, {overwriteRoutes: true})
|
||||||
|
const {getByText} = render(<K5Course {...defaultProps} defaultTab={TAB_IDS.RESOURCES} />)
|
||||||
|
waitFor(() =>
|
||||||
|
expect(getByText('Failed to load apps for Arts and Crafts.')).toBeInTheDocument()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,7 +21,7 @@ import React, {useState, useEffect} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import StaffContactInfoLayout from './StaffContactInfoLayout'
|
import StaffContactInfoLayout from './StaffContactInfoLayout'
|
||||||
import {fetchCourseInstructors, fetchCourseApps} from '@canvas/k5/react/utils'
|
import {fetchCourseInstructors, fetchCourseApps} from '@canvas/k5/react/utils'
|
||||||
import AppsList from './AppsList'
|
import AppsList from '@canvas/k5/react/AppsList'
|
||||||
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||||
|
|
||||||
const fetchStaff = cards =>
|
const fetchStaff = cards =>
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {TruncateText} from '@instructure/ui-truncate-text'
|
||||||
import {Tooltip} from '@instructure/ui-tooltip'
|
import {Tooltip} from '@instructure/ui-tooltip'
|
||||||
import {IconLtiSolid} from '@instructure/ui-icons'
|
import {IconLtiSolid} from '@instructure/ui-icons'
|
||||||
import {PresentationContent} from '@instructure/ui-a11y-content'
|
import {PresentationContent} from '@instructure/ui-a11y-content'
|
||||||
import k5Theme from '@canvas/k5/react/k5-theme'
|
import k5Theme from './k5-theme'
|
||||||
import {Flex} from '@instructure/ui-flex'
|
import {Flex} from '@instructure/ui-flex'
|
||||||
|
|
||||||
export default function K5AppLink({app}) {
|
export default function K5AppLink({app}) {
|
Loading…
Reference in New Issue