Add K-5 Manage course button and tray
This change adds a slide-out course navigation tray and associated button to the K-5 course home page for teachers. Also renames the Overview tab to "Home". closes LS-2028 flag = canvas_for_elementary Test plan: - Enroll as a teacher in a K-5 course - Go to /courses/:course_id - Expect to see a "Manage" button in the upper-left - Expect clicking that button to open a tray inside the global nav bar with the course's nav links inside it - Expect the links to be the same as the classic Canvas nav for that course, including the icons showing which links are hidden from students - Expect the order of the links to also be the same - Click a link, expect it to take you to a classic Canvas page - Click the "home" link on the classic Canvas nav, and expect it to take you back to the K-5 course home page - Also expect the "Overview" tab to be called "Home" now Change-Id: I08a375d70f98a25948073be624432edbcd1b6d04 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/262895 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
d98065dcfc
commit
c71d37fc2b
|
@ -2177,6 +2177,7 @@ class CoursesController < ApplicationController
|
||||||
js_bundle :syllabus
|
js_bundle :syllabus
|
||||||
css_bundle :syllabus, :tinymce
|
css_bundle :syllabus, :tinymce
|
||||||
when 'k5_dashboard'
|
when 'k5_dashboard'
|
||||||
|
js_env(PERMISSIONS: { manage: @context.grants_right?(@current_user, session, :manage) })
|
||||||
js_env(STUDENT_PLANNER_ENABLED: planner_enabled?)
|
js_env(STUDENT_PLANNER_ENABLED: planner_enabled?)
|
||||||
js_env(CONTEXT_MODULE_ASSIGNMENT_INFO_URL: context_url(@context, :context_context_modules_assignment_info_url))
|
js_env(CONTEXT_MODULE_ASSIGNMENT_INFO_URL: context_url(@context, :context_context_modules_assignment_info_url))
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,22 @@ if (!('IntersectionObserver' in window)) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!('ResizeObserver' in window)) {
|
||||||
|
Object.defineProperty(window, 'ResizeObserver', {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: class IntersectionObserver {
|
||||||
|
observe() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
unobserve() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!('matchMedia' in window)) {
|
if (!('matchMedia' in window)) {
|
||||||
window.matchMedia = () => ({
|
window.matchMedia = () => ({
|
||||||
matches: false,
|
matches: false,
|
||||||
|
|
|
@ -1581,6 +1581,13 @@ describe CoursesController do
|
||||||
expect(assigns[:js_env][:STUDENT_PLANNER_ENABLED]).to be_falsy
|
expect(assigns[:js_env][:STUDENT_PLANNER_ENABLED]).to be_falsy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "sets PERMISSIONS appropriately in js_env" do
|
||||||
|
user_session(@teacher)
|
||||||
|
|
||||||
|
get 'show', params: {:id => @course.id}
|
||||||
|
expect(assigns[:js_env][:PERMISSIONS]).to eq({manage: true})
|
||||||
|
end
|
||||||
|
|
||||||
it "loads announcements on home page when course is a k5 homeroom course" do
|
it "loads announcements on home page when course is a k5 homeroom course" do
|
||||||
@course.homeroom_course = true
|
@course.homeroom_course = true
|
||||||
@course.save!
|
@course.save!
|
||||||
|
|
|
@ -28,10 +28,11 @@ ready(() => {
|
||||||
if (courseContainer) {
|
if (courseContainer) {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<K5Course
|
<K5Course
|
||||||
|
canManage={ENV.PERMISSIONS.manage}
|
||||||
currentUser={ENV.current_user}
|
currentUser={ENV.current_user}
|
||||||
name={ENV.COURSE.name}
|
|
||||||
id={ENV.COURSE.id}
|
id={ENV.COURSE.id}
|
||||||
imageUrl={ENV.COURSE.image_url}
|
imageUrl={ENV.COURSE.image_url}
|
||||||
|
name={ENV.COURSE.name}
|
||||||
plannerEnabled={ENV.STUDENT_PLANNER_ENABLED}
|
plannerEnabled={ENV.STUDENT_PLANNER_ENABLED}
|
||||||
timeZone={ENV.TIMEZONE}
|
timeZone={ENV.TIMEZONE}
|
||||||
courseOverview={ENV.COURSE.course_overview}
|
courseOverview={ENV.COURSE.course_overview}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* You should have received a copy of the GNU Affero General Public License along
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import React, {useState, useEffect, useRef} from 'react'
|
import React, {useEffect, useRef, useState} from 'react'
|
||||||
import {connect, Provider} from 'react-redux'
|
import {connect, Provider} from 'react-redux'
|
||||||
import I18n from 'i18n!k5_course'
|
import I18n from 'i18n!k5_course'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
@ -26,14 +26,17 @@ import {
|
||||||
store
|
store
|
||||||
} from '@instructure/canvas-planner'
|
} from '@instructure/canvas-planner'
|
||||||
import {
|
import {
|
||||||
|
IconBankLine,
|
||||||
IconCalendarMonthLine,
|
IconCalendarMonthLine,
|
||||||
|
IconEditSolid,
|
||||||
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 {Button} from '@instructure/ui-buttons'
|
||||||
import {Heading} from '@instructure/ui-heading'
|
import {Heading} from '@instructure/ui-heading'
|
||||||
|
import {Mask} from '@instructure/ui-overlays'
|
||||||
import {TruncateText} from '@instructure/ui-truncate-text'
|
import {TruncateText} from '@instructure/ui-truncate-text'
|
||||||
import {View} from '@instructure/ui-view'
|
import {View} from '@instructure/ui-view'
|
||||||
|
|
||||||
|
@ -43,20 +46,21 @@ 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, fetchCourseApps} from '@canvas/k5/react/utils'
|
import {fetchCourseApps, fetchCourseTabs, TAB_IDS} 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 AppsList from '@canvas/k5/react/AppsList'
|
||||||
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||||
import OverviewPage from './OverviewPage'
|
import OverviewPage from './OverviewPage'
|
||||||
|
import ManageCourseTray from './ManageCourseTray'
|
||||||
|
|
||||||
const DEFAULT_COLOR = k5Theme.variables.colors.backgroundMedium
|
const DEFAULT_COLOR = k5Theme.variables.colors.backgroundMedium
|
||||||
const HERO_HEIGHT_PX = 400
|
const HERO_HEIGHT_PX = 400
|
||||||
|
|
||||||
const COURSE_TABS = [
|
const COURSE_TABS = [
|
||||||
{
|
{
|
||||||
id: TAB_IDS.OVERVIEW,
|
id: TAB_IDS.HOME,
|
||||||
icon: IconHomeLine,
|
icon: IconHomeLine,
|
||||||
label: I18n.t('Overview')
|
label: I18n.t('Home')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: TAB_IDS.SCHEDULE,
|
id: TAB_IDS.SCHEDULE,
|
||||||
|
@ -95,8 +99,7 @@ export function CourseHeaderHero({name, image, backgroundColor}) {
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
minHeight: '25vh',
|
minHeight: '25vh',
|
||||||
maxHeight: `${HERO_HEIGHT_PX}px`,
|
maxHeight: `${HERO_HEIGHT_PX}px`,
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem'
|
||||||
marginTop: '-1.25rem'
|
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
data-testid="k5-course-header-hero"
|
data-testid="k5-course-header-hero"
|
||||||
|
@ -131,17 +134,20 @@ export function K5Course({
|
||||||
assignmentsDueToday,
|
assignmentsDueToday,
|
||||||
assignmentsMissing,
|
assignmentsMissing,
|
||||||
assignmentsCompletedForToday,
|
assignmentsCompletedForToday,
|
||||||
|
courseOverview,
|
||||||
|
id,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
loadAllOpportunities,
|
loadAllOpportunities,
|
||||||
name,
|
name,
|
||||||
id,
|
|
||||||
timeZone,
|
timeZone,
|
||||||
defaultTab = TAB_IDS.OVERVIEW,
|
canManage = false,
|
||||||
plannerEnabled = false,
|
defaultTab = TAB_IDS.HOME,
|
||||||
courseOverview
|
plannerEnabled = false
|
||||||
}) {
|
}) {
|
||||||
const {activeTab, currentTab, handleTabChange} = useTabState(defaultTab)
|
const {activeTab, currentTab, handleTabChange} = useTabState(defaultTab)
|
||||||
|
const [courseNavLinks, setCourseNavLinks] = useState([])
|
||||||
const [tabsRef, setTabsRef] = useState(null)
|
const [tabsRef, setTabsRef] = useState(null)
|
||||||
|
const [trayOpen, setTrayOpen] = useState(false)
|
||||||
const plannerInitialized = usePlanner({
|
const plannerInitialized = usePlanner({
|
||||||
plannerEnabled,
|
plannerEnabled,
|
||||||
isPlannerActive: () => activeTab.current === TAB_IDS.SCHEDULE,
|
isPlannerActive: () => activeTab.current === TAB_IDS.SCHEDULE,
|
||||||
|
@ -169,8 +175,14 @@ export function K5Course({
|
||||||
.then(setApps)
|
.then(setApps)
|
||||||
.catch(showFlashError(I18n.t('Failed to load apps for %{name}.', {name})))
|
.catch(showFlashError(I18n.t('Failed to load apps for %{name}.', {name})))
|
||||||
.finally(() => setAppsLoading(false))
|
.finally(() => setAppsLoading(false))
|
||||||
|
fetchCourseTabs(id)
|
||||||
|
.then(setCourseNavLinks)
|
||||||
|
.catch(showFlashError(I18n.t('Failed to load course navigation for %{name}.', {name})))
|
||||||
}, [id, name])
|
}, [id, name])
|
||||||
|
|
||||||
|
const handleOpenTray = () => setTrayOpen(true)
|
||||||
|
const handleCloseTray = () => setTrayOpen(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<K5DashboardContext.Provider
|
<K5DashboardContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -181,12 +193,28 @@ export function K5Course({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View as="section">
|
<View as="section">
|
||||||
|
{trayOpen && <Mask onClick={handleCloseTray} fullscreen />}
|
||||||
|
{canManage && (
|
||||||
|
<ManageCourseTray navLinks={courseNavLinks} open={trayOpen} onClose={handleCloseTray} />
|
||||||
|
)}
|
||||||
<K5Tabs
|
<K5Tabs
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
tabs={COURSE_TABS}
|
tabs={COURSE_TABS}
|
||||||
tabsRef={setTabsRef}
|
tabsRef={setTabsRef}
|
||||||
>
|
>
|
||||||
|
{canManage && (
|
||||||
|
<View
|
||||||
|
as="section"
|
||||||
|
borderWidth="0 0 small 0"
|
||||||
|
padding="0 0 medium 0"
|
||||||
|
margin="0 0 medium 0"
|
||||||
|
>
|
||||||
|
<Button onClick={handleOpenTray} renderIcon={<IconEditSolid />}>
|
||||||
|
{I18n.t('Manage')}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
<CourseHeaderHero name={name} image={imageUrl} backgroundColor={DEFAULT_COLOR} />
|
<CourseHeaderHero name={name} image={imageUrl} backgroundColor={DEFAULT_COLOR} />
|
||||||
</K5Tabs>
|
</K5Tabs>
|
||||||
{currentTab === TAB_IDS.OVERVIEW && <OverviewPage content={courseOverview} />}
|
{currentTab === TAB_IDS.OVERVIEW && <OverviewPage content={courseOverview} />}
|
||||||
|
@ -202,10 +230,11 @@ K5Course.propTypes = {
|
||||||
assignmentsDueToday: PropTypes.object.isRequired,
|
assignmentsDueToday: PropTypes.object.isRequired,
|
||||||
assignmentsMissing: PropTypes.object.isRequired,
|
assignmentsMissing: PropTypes.object.isRequired,
|
||||||
assignmentsCompletedForToday: PropTypes.object.isRequired,
|
assignmentsCompletedForToday: PropTypes.object.isRequired,
|
||||||
|
id: PropTypes.string.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,
|
||||||
|
canManage: PropTypes.bool,
|
||||||
defaultTab: PropTypes.string,
|
defaultTab: PropTypes.string,
|
||||||
imageUrl: PropTypes.string,
|
imageUrl: PropTypes.string,
|
||||||
plannerEnabled: PropTypes.bool,
|
plannerEnabled: PropTypes.bool,
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 - present Instructure, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of Canvas.
|
||||||
|
*
|
||||||
|
* Canvas is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useEffect, useRef, useState} from 'react'
|
||||||
|
import I18n from 'i18n!k5_manage_course_tray'
|
||||||
|
|
||||||
|
import {CloseButton} from '@instructure/ui-buttons'
|
||||||
|
import {Flex} from '@instructure/ui-flex'
|
||||||
|
import {IconOffLine} from '@instructure/ui-icons'
|
||||||
|
import {Link} from '@instructure/ui-link'
|
||||||
|
import {Text} from '@instructure/ui-text'
|
||||||
|
import {Tray} from '@instructure/ui-tray'
|
||||||
|
import {Tooltip} from '@instructure/ui-tooltip'
|
||||||
|
import {View} from '@instructure/ui-view'
|
||||||
|
|
||||||
|
export default function ManageCourseTray({navLinks, onClose, open}) {
|
||||||
|
const globalNavRef = useRef()
|
||||||
|
const globalNavObserverRef = useRef()
|
||||||
|
const [offset, setOffset] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
globalNavRef.current = document.getElementById('header')
|
||||||
|
setOffset(globalNavRef.current?.getBoundingClientRect()?.width || 0)
|
||||||
|
globalNavObserverRef.current = new ResizeObserver(entries => {
|
||||||
|
entries.forEach(entry => setOffset(entry.contentRect.width))
|
||||||
|
})
|
||||||
|
globalNavObserverRef.current.observe(globalNavRef.current)
|
||||||
|
return () => globalNavObserverRef.current?.unobserve(globalNavRef.current)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tray
|
||||||
|
label={I18n.t('Course Navigation Tray')}
|
||||||
|
onDismiss={onClose}
|
||||||
|
open={open}
|
||||||
|
placement="start"
|
||||||
|
size="regular"
|
||||||
|
theme={{
|
||||||
|
regularWidth: '26em',
|
||||||
|
zIndex: 99
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{marginLeft: offset}}>
|
||||||
|
<View as="section" padding="small medium">
|
||||||
|
<Flex direction="row" justifyItems="end" margin="medium small">
|
||||||
|
<Flex.Item margin="0 0 0 small">
|
||||||
|
<CloseButton onClick={onClose}>{I18n.t('Close')}</CloseButton>
|
||||||
|
</Flex.Item>
|
||||||
|
</Flex>
|
||||||
|
{navLinks.map(link => (
|
||||||
|
<View as="div" margin="small" key={`course-nav-${link.id}`}>
|
||||||
|
<Flex direction="row">
|
||||||
|
<Flex.Item grow shrink>
|
||||||
|
<Link
|
||||||
|
href={link.html_url}
|
||||||
|
theme={{
|
||||||
|
hoverTextDecorationWithinText: 'underline',
|
||||||
|
textDecorationWithinText: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="medium">{link.label}</Text>
|
||||||
|
</Link>
|
||||||
|
</Flex.Item>
|
||||||
|
{link.visibility === 'admins' && link.id !== 'settings' && (
|
||||||
|
<Flex.Item>
|
||||||
|
<Tooltip
|
||||||
|
renderTip={I18n.t('Disabled. Not visible to students')}
|
||||||
|
on={['hover', 'focus']}
|
||||||
|
offsetY={6}
|
||||||
|
>
|
||||||
|
<IconOffLine
|
||||||
|
size="small"
|
||||||
|
theme={{sizeSmall: '1.5rem'}}
|
||||||
|
data-testid="k5-course-nav-hidden-icon"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex.Item>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</div>
|
||||||
|
</Tray>
|
||||||
|
)
|
||||||
|
}
|
|
@ -18,9 +18,10 @@
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import moxios from 'moxios'
|
import moxios from 'moxios'
|
||||||
import {render, waitFor} from '@testing-library/react'
|
import {act, fireEvent, render, waitFor} from '@testing-library/react'
|
||||||
import {K5Course} from '../K5Course'
|
import {K5Course} from '../K5Course'
|
||||||
import fetchMock from 'fetch-mock'
|
import fetchMock from 'fetch-mock'
|
||||||
|
import {MOCK_COURSE_APPS, MOCK_COURSE_TABS} from './mocks'
|
||||||
import {TAB_IDS} from '@canvas/k5/react/utils'
|
import {TAB_IDS} from '@canvas/k5/react/utils'
|
||||||
|
|
||||||
const currentUser = {
|
const currentUser = {
|
||||||
|
@ -45,22 +46,16 @@ const defaultProps = {
|
||||||
loadAllOpportunities: () => {},
|
loadAllOpportunities: () => {},
|
||||||
name: 'Arts and Crafts',
|
name: 'Arts and Crafts',
|
||||||
id: '30',
|
id: '30',
|
||||||
timeZone: defaultEnv.TIMEZONE
|
timeZone: defaultEnv.TIMEZONE,
|
||||||
|
canManage: false
|
||||||
}
|
}
|
||||||
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'
|
const FETCH_APPS_URL = '/api/v1/courses/30/external_tools/visible_course_nav_tools'
|
||||||
|
const FETCH_TABS_URL = '/api/v1/courses/30/tabs'
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
moxios.install()
|
moxios.install()
|
||||||
fetchMock.get(FETCH_APPS_URL, JSON.stringify(fetchAppsResponse))
|
fetchMock.get(FETCH_APPS_URL, JSON.stringify(MOCK_COURSE_APPS))
|
||||||
|
fetchMock.get(FETCH_TABS_URL, JSON.stringify(MOCK_COURSE_TABS))
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
@ -99,16 +94,66 @@ describe('K-5 Subject Course', () => {
|
||||||
expect(getByText(defaultProps.name)).toBeInTheDocument()
|
expect(getByText(defaultProps.name)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows Overview, Schedule, Modules, Grades, and Resources options', () => {
|
it('shows Home, Schedule, Modules, Grades, and Resources options', () => {
|
||||||
const {getByText} = render(<K5Course {...defaultProps} />)
|
const {getByText} = render(<K5Course {...defaultProps} />)
|
||||||
;['Overview', 'Schedule', 'Modules', 'Grades', 'Resources'].forEach(label =>
|
;['Home', 'Schedule', 'Modules', 'Grades', 'Resources'].forEach(label =>
|
||||||
expect(getByText(label)).toBeInTheDocument()
|
expect(getByText(label)).toBeInTheDocument()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('defaults to the Overview tab', () => {
|
it('defaults to the Home tab', () => {
|
||||||
const {getByRole} = render(<K5Course {...defaultProps} />)
|
const {getByRole} = render(<K5Course {...defaultProps} />)
|
||||||
expect(getByRole('tab', {name: 'Overview', selected: true})).toBeInTheDocument()
|
expect(getByRole('tab', {name: 'Home', selected: true})).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Manage course functionality', () => {
|
||||||
|
it('Shows a manage button when the user has manage permissions', () => {
|
||||||
|
const {getByRole} = render(<K5Course {...defaultProps} canManage />)
|
||||||
|
expect(getByRole('button', {name: 'Manage'})).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('The manage button opens a slide-out tray with the course navigation tabs when clicked', async () => {
|
||||||
|
const {getByRole} = render(<K5Course {...defaultProps} canManage />)
|
||||||
|
const manageButton = getByRole('button', {name: 'Manage'})
|
||||||
|
|
||||||
|
act(() => manageButton.click())
|
||||||
|
|
||||||
|
const validateLink = (name, href) => {
|
||||||
|
const link = getByRole('link', {name})
|
||||||
|
expect(link).toBeInTheDocument()
|
||||||
|
expect(link.href).toBe(href)
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
validateLink('Home', 'http://localhost/courses/30')
|
||||||
|
validateLink('Modules', 'http://localhost/courses/30/modules')
|
||||||
|
validateLink('Assignments', 'http://localhost/courses/30/assignments')
|
||||||
|
validateLink('Settings', 'http://localhost/courses/30/settings')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Displays an icon indicating that a nav link is hidden from students', async () => {
|
||||||
|
const {findAllByTestId, getByRole, getByText} = render(
|
||||||
|
<K5Course {...defaultProps} canManage />
|
||||||
|
)
|
||||||
|
const manageButton = getByRole('button', {name: 'Manage'})
|
||||||
|
|
||||||
|
act(() => manageButton.click())
|
||||||
|
|
||||||
|
const hiddenIcons = await findAllByTestId('k5-course-nav-hidden-icon')
|
||||||
|
// Doesn't show the icon for settings, though
|
||||||
|
expect(hiddenIcons.length).toBe(1)
|
||||||
|
|
||||||
|
fireEvent.mouseOver(hiddenIcons[0])
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getByText('Disabled. Not visible to students')).toBeInTheDocument()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Does not show a manage button when the user does not have manage permissions', () => {
|
||||||
|
const {queryByRole} = render(<K5Course {...defaultProps} />)
|
||||||
|
expect(queryByRole('button', {name: 'Manage'})).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 - present Instructure, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of Canvas.
|
||||||
|
*
|
||||||
|
* Canvas is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const MOCK_COURSE_APPS = [
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
course_navigation: {
|
||||||
|
text: 'Studio',
|
||||||
|
icon_url: 'studio.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const MOCK_COURSE_TABS = [
|
||||||
|
{
|
||||||
|
id: 'home',
|
||||||
|
html_url: '/courses/30',
|
||||||
|
label: 'Home',
|
||||||
|
visibility: 'public'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'modules',
|
||||||
|
html_url: '/courses/30/modules',
|
||||||
|
label: 'Modules',
|
||||||
|
visibility: 'public'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'assignments',
|
||||||
|
html_url: '/courses/30/assignments',
|
||||||
|
label: 'Assignments',
|
||||||
|
visibility: 'admins',
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
html_url: '/courses/30/settings',
|
||||||
|
label: 'Settings',
|
||||||
|
visibility: 'admins'
|
||||||
|
}
|
||||||
|
]
|
|
@ -64,9 +64,9 @@ const K5Tabs = ({children, currentTab, name, onTabChange, tabs, tabsRef}) => {
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={{backgroundColor: k5Theme.variables.colors.background.backgroundLightest}}
|
style={{backgroundColor: k5Theme.variables.colors.background.backgroundLightest}}
|
||||||
>
|
>
|
||||||
<View as="div" padding="medium 0 0 0" borderWidth="none none small none">
|
<View as="div" borderWidth="none none small none">
|
||||||
{name && (
|
{name && (
|
||||||
<Heading as="h1" level={sticky ? 'h2' : 'h1'} margin="0 0 small 0">
|
<Heading as="h1" level={sticky ? 'h2' : 'h1'} margin="medium 0 small 0">
|
||||||
{I18n.t('Welcome, %{name}!', {name})}
|
{I18n.t('Welcome, %{name}!', {name})}
|
||||||
</Heading>
|
</Heading>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -75,10 +75,10 @@ describe('useTabState hook', () => {
|
||||||
expect(window.history.replaceState.mock.calls[0][0].id).toBe(TAB_IDS.RESOURCES)
|
expect(window.history.replaceState.mock.calls[0][0].id).toBe(TAB_IDS.RESOURCES)
|
||||||
expect(window.history.replaceState.mock.calls[0][2]).toBe('http://localhost/#resources')
|
expect(window.history.replaceState.mock.calls[0][2]).toBe('http://localhost/#resources')
|
||||||
|
|
||||||
act(() => result.current.handleTabChange(TAB_IDS.OVERVIEW))
|
act(() => result.current.handleTabChange(TAB_IDS.HOME))
|
||||||
|
|
||||||
expect(window.history.replaceState.mock.calls[1][0].id).toBe(TAB_IDS.OVERVIEW)
|
expect(window.history.replaceState.mock.calls[1][0].id).toBe(TAB_IDS.HOME)
|
||||||
expect(window.history.replaceState.mock.calls[1][2]).toBe('http://localhost/#overview')
|
expect(window.history.replaceState.mock.calls[1][2]).toBe('http://localhost/#home')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -105,6 +105,9 @@ export const fetchCourseApps = courseId =>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const fetchCourseTabs = courseId =>
|
||||||
|
asJson(window.fetch(`/api/v1/courses/${courseId}/tabs`, defaultFetchOptions))
|
||||||
|
|
||||||
export const readableRoleName = role => {
|
export const readableRoleName = role => {
|
||||||
const ROLES = {
|
const ROLES = {
|
||||||
TeacherEnrollment: I18n.t('Teacher'),
|
TeacherEnrollment: I18n.t('Teacher'),
|
||||||
|
@ -126,10 +129,10 @@ export const sendMessage = (recipientId, message, subject) =>
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TAB_IDS = {
|
export const TAB_IDS = {
|
||||||
|
HOME: 'tab-home',
|
||||||
HOMEROOM: 'tab-homeroom',
|
HOMEROOM: 'tab-homeroom',
|
||||||
SCHEDULE: 'tab-schedule',
|
SCHEDULE: 'tab-schedule',
|
||||||
GRADES: 'tab-grades',
|
GRADES: 'tab-grades',
|
||||||
RESOURCES: 'tab-resources',
|
RESOURCES: 'tab-resources',
|
||||||
OVERVIEW: 'tab-overview',
|
|
||||||
MODULES: 'tab-modules'
|
MODULES: 'tab-modules'
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue