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
|
||||
css_bundle :syllabus, :tinymce
|
||||
when 'k5_dashboard'
|
||||
js_env(PERMISSIONS: { manage: @context.grants_right?(@current_user, session, :manage) })
|
||||
js_env(STUDENT_PLANNER_ENABLED: planner_enabled?)
|
||||
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)) {
|
||||
window.matchMedia = () => ({
|
||||
matches: false,
|
||||
|
|
|
@ -1581,6 +1581,13 @@ describe CoursesController do
|
|||
expect(assigns[:js_env][:STUDENT_PLANNER_ENABLED]).to be_falsy
|
||||
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
|
||||
@course.homeroom_course = true
|
||||
@course.save!
|
||||
|
|
|
@ -28,10 +28,11 @@ ready(() => {
|
|||
if (courseContainer) {
|
||||
ReactDOM.render(
|
||||
<K5Course
|
||||
canManage={ENV.PERMISSIONS.manage}
|
||||
currentUser={ENV.current_user}
|
||||
name={ENV.COURSE.name}
|
||||
id={ENV.COURSE.id}
|
||||
imageUrl={ENV.COURSE.image_url}
|
||||
name={ENV.COURSE.name}
|
||||
plannerEnabled={ENV.STUDENT_PLANNER_ENABLED}
|
||||
timeZone={ENV.TIMEZONE}
|
||||
courseOverview={ENV.COURSE.course_overview}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* 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, {useState, useEffect, useRef} from 'react'
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import {connect, Provider} from 'react-redux'
|
||||
import I18n from 'i18n!k5_course'
|
||||
import PropTypes from 'prop-types'
|
||||
|
@ -26,14 +26,17 @@ import {
|
|||
store
|
||||
} from '@instructure/canvas-planner'
|
||||
import {
|
||||
IconBankLine,
|
||||
IconCalendarMonthLine,
|
||||
IconEditSolid,
|
||||
IconHomeLine,
|
||||
IconModuleLine,
|
||||
IconStarLightLine,
|
||||
IconBankLine
|
||||
IconStarLightLine
|
||||
} from '@instructure/ui-icons'
|
||||
import {ApplyTheme} from '@instructure/ui-themeable'
|
||||
import {Button} from '@instructure/ui-buttons'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {Mask} from '@instructure/ui-overlays'
|
||||
import {TruncateText} from '@instructure/ui-truncate-text'
|
||||
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 useTabState from '@canvas/k5/react/hooks/useTabState'
|
||||
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 AppsList from '@canvas/k5/react/AppsList'
|
||||
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||
import OverviewPage from './OverviewPage'
|
||||
import ManageCourseTray from './ManageCourseTray'
|
||||
|
||||
const DEFAULT_COLOR = k5Theme.variables.colors.backgroundMedium
|
||||
const HERO_HEIGHT_PX = 400
|
||||
|
||||
const COURSE_TABS = [
|
||||
{
|
||||
id: TAB_IDS.OVERVIEW,
|
||||
id: TAB_IDS.HOME,
|
||||
icon: IconHomeLine,
|
||||
label: I18n.t('Overview')
|
||||
label: I18n.t('Home')
|
||||
},
|
||||
{
|
||||
id: TAB_IDS.SCHEDULE,
|
||||
|
@ -95,8 +99,7 @@ export function CourseHeaderHero({name, image, backgroundColor}) {
|
|||
borderRadius: '8px',
|
||||
minHeight: '25vh',
|
||||
maxHeight: `${HERO_HEIGHT_PX}px`,
|
||||
marginBottom: '1rem',
|
||||
marginTop: '-1.25rem'
|
||||
marginBottom: '1rem'
|
||||
}}
|
||||
aria-hidden="true"
|
||||
data-testid="k5-course-header-hero"
|
||||
|
@ -131,17 +134,20 @@ export function K5Course({
|
|||
assignmentsDueToday,
|
||||
assignmentsMissing,
|
||||
assignmentsCompletedForToday,
|
||||
courseOverview,
|
||||
id,
|
||||
imageUrl,
|
||||
loadAllOpportunities,
|
||||
name,
|
||||
id,
|
||||
timeZone,
|
||||
defaultTab = TAB_IDS.OVERVIEW,
|
||||
plannerEnabled = false,
|
||||
courseOverview
|
||||
canManage = false,
|
||||
defaultTab = TAB_IDS.HOME,
|
||||
plannerEnabled = false
|
||||
}) {
|
||||
const {activeTab, currentTab, handleTabChange} = useTabState(defaultTab)
|
||||
const [courseNavLinks, setCourseNavLinks] = useState([])
|
||||
const [tabsRef, setTabsRef] = useState(null)
|
||||
const [trayOpen, setTrayOpen] = useState(false)
|
||||
const plannerInitialized = usePlanner({
|
||||
plannerEnabled,
|
||||
isPlannerActive: () => activeTab.current === TAB_IDS.SCHEDULE,
|
||||
|
@ -169,8 +175,14 @@ export function K5Course({
|
|||
.then(setApps)
|
||||
.catch(showFlashError(I18n.t('Failed to load apps for %{name}.', {name})))
|
||||
.finally(() => setAppsLoading(false))
|
||||
fetchCourseTabs(id)
|
||||
.then(setCourseNavLinks)
|
||||
.catch(showFlashError(I18n.t('Failed to load course navigation for %{name}.', {name})))
|
||||
}, [id, name])
|
||||
|
||||
const handleOpenTray = () => setTrayOpen(true)
|
||||
const handleCloseTray = () => setTrayOpen(false)
|
||||
|
||||
return (
|
||||
<K5DashboardContext.Provider
|
||||
value={{
|
||||
|
@ -181,12 +193,28 @@ export function K5Course({
|
|||
}}
|
||||
>
|
||||
<View as="section">
|
||||
{trayOpen && <Mask onClick={handleCloseTray} fullscreen />}
|
||||
{canManage && (
|
||||
<ManageCourseTray navLinks={courseNavLinks} open={trayOpen} onClose={handleCloseTray} />
|
||||
)}
|
||||
<K5Tabs
|
||||
currentTab={currentTab}
|
||||
onTabChange={handleTabChange}
|
||||
tabs={COURSE_TABS}
|
||||
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} />
|
||||
</K5Tabs>
|
||||
{currentTab === TAB_IDS.OVERVIEW && <OverviewPage content={courseOverview} />}
|
||||
|
@ -202,10 +230,11 @@ K5Course.propTypes = {
|
|||
assignmentsDueToday: PropTypes.object.isRequired,
|
||||
assignmentsMissing: PropTypes.object.isRequired,
|
||||
assignmentsCompletedForToday: PropTypes.object.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
loadAllOpportunities: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
timeZone: PropTypes.string.isRequired,
|
||||
canManage: PropTypes.bool,
|
||||
defaultTab: PropTypes.string,
|
||||
imageUrl: PropTypes.string,
|
||||
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 moxios from 'moxios'
|
||||
import {render, waitFor} from '@testing-library/react'
|
||||
import {act, fireEvent, render, waitFor} from '@testing-library/react'
|
||||
import {K5Course} from '../K5Course'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {MOCK_COURSE_APPS, MOCK_COURSE_TABS} from './mocks'
|
||||
import {TAB_IDS} from '@canvas/k5/react/utils'
|
||||
|
||||
const currentUser = {
|
||||
|
@ -45,22 +46,16 @@ const defaultProps = {
|
|||
loadAllOpportunities: () => {},
|
||||
name: 'Arts and Crafts',
|
||||
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_TABS_URL = '/api/v1/courses/30/tabs'
|
||||
|
||||
beforeAll(() => {
|
||||
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(() => {
|
||||
|
@ -99,16 +94,66 @@ describe('K-5 Subject Course', () => {
|
|||
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} />)
|
||||
;['Overview', 'Schedule', 'Modules', 'Grades', 'Resources'].forEach(label =>
|
||||
;['Home', 'Schedule', 'Modules', 'Grades', 'Resources'].forEach(label =>
|
||||
expect(getByText(label)).toBeInTheDocument()
|
||||
)
|
||||
})
|
||||
|
||||
it('defaults to the Overview tab', () => {
|
||||
it('defaults to the Home tab', () => {
|
||||
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}
|
||||
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 && (
|
||||
<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})}
|
||||
</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][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][2]).toBe('http://localhost/#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/#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 => {
|
||||
const ROLES = {
|
||||
TeacherEnrollment: I18n.t('Teacher'),
|
||||
|
@ -126,10 +129,10 @@ export const sendMessage = (recipientId, message, subject) =>
|
|||
})
|
||||
|
||||
export const TAB_IDS = {
|
||||
HOME: 'tab-home',
|
||||
HOMEROOM: 'tab-homeroom',
|
||||
SCHEDULE: 'tab-schedule',
|
||||
GRADES: 'tab-grades',
|
||||
RESOURCES: 'tab-resources',
|
||||
OVERVIEW: 'tab-overview',
|
||||
MODULES: 'tab-modules'
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue