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:
Jackson Howe 2021-04-09 09:37:02 -06:00
parent 86c6f99285
commit e1ccc28388
8 changed files with 79 additions and 6 deletions

View File

@ -30,6 +30,7 @@ ready(() => {
<K5Course
currentUser={ENV.current_user}
name={ENV.COURSE.name}
id={ENV.COURSE.id}
imageUrl={ENV.COURSE.image_url}
plannerEnabled={ENV.STUDENT_PLANNER_ENABLED}
timeZone={ENV.TIMEZONE}

View File

@ -29,7 +29,8 @@ import {
IconCalendarMonthLine,
IconHomeLine,
IconModuleLine,
IconStarLightLine
IconStarLightLine,
IconBankLine
} from '@instructure/ui-icons'
import {ApplyTheme} from '@instructure/ui-themeable'
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 useTabState from '@canvas/k5/react/hooks/useTabState'
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 AppsList from '@canvas/k5/react/AppsList'
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
const DEFAULT_COLOR = k5Theme.variables.colors.backgroundMedium
const HERO_HEIGHT_PX = 400
@ -68,6 +71,11 @@ const COURSE_TABS = [
id: TAB_IDS.GRADES,
icon: IconStarLightLine,
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({
assignmentsDueToday,
assignmentsMissing,
@ -115,6 +133,7 @@ export function K5Course({
imageUrl,
loadAllOpportunities,
name,
id,
timeZone,
defaultTab = TAB_IDS.OVERVIEW,
plannerEnabled = false
@ -128,6 +147,8 @@ export function K5Course({
callback: () => loadAllOpportunities(),
singleCourse: true
})
const [apps, setApps] = useState([])
const [isAppsLoading, setAppsLoading] = useState(false)
const modulesRef = useRef(null)
useEffect(() => {
@ -140,6 +161,14 @@ export function K5Course({
}
}, [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 (
<K5DashboardContext.Provider
value={{
@ -160,6 +189,7 @@ export function K5Course({
</K5Tabs>
{plannerInitialized && <SchedulePage visible={currentTab === TAB_IDS.SCHEDULE} />}
{!plannerEnabled && currentTab === TAB_IDS.SCHEDULE && createTeacherPreview(timeZone)}
{currentTab === TAB_IDS.RESOURCES && <AppsList isLoading={isAppsLoading} apps={apps} />}
</View>
</K5DashboardContext.Provider>
)
@ -171,6 +201,7 @@ K5Course.propTypes = {
assignmentsCompletedForToday: PropTypes.object.isRequired,
loadAllOpportunities: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
timeZone: PropTypes.string.isRequired,
defaultTab: PropTypes.string,
imageUrl: PropTypes.string,

View File

@ -20,6 +20,8 @@ import React from 'react'
import moxios from 'moxios'
import {render, waitFor} from '@testing-library/react'
import {K5Course} from '../K5Course'
import fetchMock from 'fetch-mock'
import {TAB_IDS} from '@canvas/k5/react/utils'
const currentUser = {
id: '1',
@ -42,15 +44,28 @@ const defaultProps = {
currentUser,
loadAllOpportunities: () => {},
name: 'Arts and Crafts',
id: '30',
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(() => {
moxios.install()
fetchMock.get(FETCH_APPS_URL, JSON.stringify(fetchAppsResponse))
})
afterAll(() => {
moxios.uninstall()
fetchMock.restore()
})
beforeEach(() => {
@ -84,9 +99,9 @@ describe('K-5 Subject Course', () => {
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} />)
;['Overview', 'Schedule', 'Modules', 'Grades'].forEach(label =>
;['Overview', 'Schedule', 'Modules', 'Grades', 'Resources'].forEach(label =>
expect(getByText(label)).toBeInTheDocument()
)
})
@ -108,4 +123,30 @@ describe('K-5 Subject Course', () => {
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()
)
})
})
})

View File

@ -21,7 +21,7 @@ import React, {useState, useEffect} from 'react'
import PropTypes from 'prop-types'
import StaffContactInfoLayout from './StaffContactInfoLayout'
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'
const fetchStaff = cards =>

View File

@ -28,7 +28,7 @@ import {TruncateText} from '@instructure/ui-truncate-text'
import {Tooltip} from '@instructure/ui-tooltip'
import {IconLtiSolid} from '@instructure/ui-icons'
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'
export default function K5AppLink({app}) {