diff --git a/packages/app/obojobo-repository/client/css/_defaults.scss b/packages/app/obojobo-repository/client/css/_defaults.scss
index a8326fc902..cb167286a0 100644
--- a/packages/app/obojobo-repository/client/css/_defaults.scss
+++ b/packages/app/obojobo-repository/client/css/_defaults.scss
@@ -33,6 +33,11 @@ $color-reward-text: #947d00;
$color-obojobo-blue: #0d4fa7;
$color-preview: #af1b5c;
+$color-nav-highlight: $color-obojobo-blue;
+$color-nav-hover: darken($color-highlight, 25%);
+$color-nav-bg-selected: lighten($color-obojobo-blue, 62%);
+$color-nav-bg-hover: lighten($color-obojobo-blue, 58%);
+
$size-spacing-vertical-big: 40px;
$size-spacing-vertical-half: $size-spacing-vertical-big / 2;
$size-spacing-vertical-internal: $size-spacing-vertical-half / 2;
diff --git a/packages/app/obojobo-repository/server/routes/stats.js b/packages/app/obojobo-repository/server/routes/stats.js
index 766d8370ad..9d9b765189 100644
--- a/packages/app/obojobo-repository/server/routes/stats.js
+++ b/packages/app/obojobo-repository/server/routes/stats.js
@@ -23,4 +23,18 @@ router
res.render('pages/page-stats-server.jsx', props)
})
+router
+ .route('/statsCourses')
+ .get([requireCurrentUser, requireCanViewStatsPage])
+ .get((req, res) => {
+ const props = {
+ title: 'StatsCourses',
+ currentUser: req.currentUser,
+ // must use webpackAssetPath for all webpack assets to work in dev and production!
+ appCSSUrl: webpackAssetPath('stats.css'),
+ appJsUrl: webpackAssetPath('stats.js')
+ }
+ res.render('pages/page-stats-server.jsx', props)
+ })
+
module.exports = router
diff --git a/packages/app/obojobo-repository/server/routes/stats.test.js b/packages/app/obojobo-repository/server/routes/stats.test.js
index 94d2b0ee7c..0dbfdb84f1 100644
--- a/packages/app/obojobo-repository/server/routes/stats.test.js
+++ b/packages/app/obojobo-repository/server/routes/stats.test.js
@@ -112,4 +112,33 @@ describe('repository stats route', () => {
expect(response.statusCode).toBe(200)
})
})
+
+ test('get /statsCourses returns a "not authorized" if the viewer does not have canViewStatsPage', () => {
+ // always return false - a.k.a. the user does not have the right perms to use this
+ mockCurrentUser.hasPermission = () => false
+
+ expect.hasAssertions()
+
+ return request(app)
+ .get('/statsCourses')
+ .then(response => {
+ expect(mockStatsComponent).toHaveBeenCalledTimes(0)
+ expect(response.statusCode).toBe(401)
+ })
+ })
+
+ test('get /statsCourses sends the correct props to the Stats component when the user has canViewStatsPage', () => {
+ expect.hasAssertions()
+
+ return request(app)
+ .get('/statsCourses')
+ .then(response => {
+ expect(mockStatsComponent).toHaveBeenCalledTimes(1)
+ expect(mockStatsComponentConstructor).toHaveBeenCalledWith({
+ title: 'StatsCourses',
+ currentUser: mockCurrentUser
+ })
+ expect(response.statusCode).toBe(200)
+ })
+ })
})
diff --git a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js
index c9071c48eb..46cddee7b2 100644
--- a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js
+++ b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js
@@ -2,7 +2,11 @@ const { MODE_RECENT, MODE_ALL, MODE_COLLECTION } = require('../repository-consta
const debouncePromise = require('debounce-promise')
const dayjs = require('dayjs')
const advancedFormat = require('dayjs/plugin/advancedFormat')
-const { apiGetAssessmentDetailsForDraft } = require('./shared-api-methods')
+const {
+ apiGetAssessmentDetailsForDraft,
+ apiGetCoursesForDraft,
+ apiGetAssessmentDetailsForCourse
+} = require('./shared-api-methods')
dayjs.extend(advancedFormat)
// =================== API =======================
@@ -232,6 +236,20 @@ const showAssessmentScoreData = module => ({
promise: apiGetAssessmentDetailsForDraft(module.draftId)
})
+const SHOW_COURSES_BY_DRAFT = 'SHOW_COURSES_BY_DRAFT'
+const showCoursesByDraft = module => ({
+ type: SHOW_COURSES_BY_DRAFT,
+ meta: { module },
+ promise: apiGetCoursesForDraft(module.draftId)
+})
+
+const SHOW_ASSESSMENTS_BY_COURSE = 'SHOW_ASSESSMENTS_BY_COURSE'
+const showCourseAssessmentData = module => ({
+ type: SHOW_ASSESSMENTS_BY_COURSE,
+ meta: { module },
+ promise: apiGetAssessmentDetailsForCourse(module.draftId, module.courseId)
+})
+
const RESTORE_VERSION = 'RESTORE_VERSION'
const restoreVersion = (draftId, versionId) => ({
type: RESTORE_VERSION,
@@ -664,6 +682,8 @@ module.exports = {
IMPORT_MODULE_FILE,
CHECK_MODULE_LOCK,
SHOW_ASSESSMENT_SCORE_DATA,
+ SHOW_COURSES_BY_DRAFT,
+ SHOW_ASSESSMENTS_BY_COURSE,
GET_DELETED_MODULES,
GET_MODULES,
BULK_RESTORE_MODULES,
@@ -705,6 +725,8 @@ module.exports = {
importModuleFile,
checkModuleLock,
showAssessmentScoreData,
+ showCoursesByDraft,
+ showCourseAssessmentData,
getDeletedModules,
getModules,
bulkRestoreModules
diff --git a/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js b/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js
index 97c8e4460c..f5a4378597 100644
--- a/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js
+++ b/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js
@@ -2,7 +2,9 @@ const dayjs = require('dayjs')
const advancedFormat = require('dayjs/plugin/advancedFormat')
jest.mock('./shared-api-methods', () => ({
- apiGetAssessmentDetailsForDraft: () => Promise.resolve()
+ apiGetAssessmentDetailsForDraft: () => Promise.resolve(),
+ apiGetCoursesForDraft: () => Promise.resolve(),
+ apiGetAssessmentDetailsForCourse: () => Promise.resolve()
}))
dayjs.extend(advancedFormat)
@@ -1767,6 +1769,30 @@ describe('Dashboard Actions', () => {
})
})
+ test('showCourseByDraft returns expected object', async () => {
+ const result = await DashboardActions.showCoursesByDraft(['draft-id-1', 'draft-id-2'])
+
+ expect(result).toEqual({
+ type: 'SHOW_COURSES_BY_DRAFT',
+ meta: {
+ module: ['draft-id-1', 'draft-id-2']
+ },
+ promise: expect.any(Promise)
+ })
+ })
+
+ test('showCourseAssessmentData returns expected object', async () => {
+ const result = await DashboardActions.showCourseAssessmentData(['draft-id-1', 'draft-id-2'])
+
+ expect(result).toEqual({
+ type: 'SHOW_ASSESSMENTS_BY_COURSE',
+ meta: {
+ module: ['draft-id-1', 'draft-id-2']
+ },
+ promise: expect.any(Promise)
+ })
+ })
+
test('restoreVersion makes all other sub-calls - draft json as object, restoration errors', () => {
// numerous API calls are made in sequence
const mockFetchJSON = jest
diff --git a/packages/app/obojobo-repository/shared/actions/shared-api-methods.js b/packages/app/obojobo-repository/shared/actions/shared-api-methods.js
index 0a990b25a2..49a8441416 100644
--- a/packages/app/obojobo-repository/shared/actions/shared-api-methods.js
+++ b/packages/app/obojobo-repository/shared/actions/shared-api-methods.js
@@ -21,7 +21,24 @@ const apiGetAssessmentDetailsForDraft = draftId => {
const apiGetAssessmentDetailsForMultipleDrafts = draftIds =>
Promise.all(draftIds.map(id => apiGetAssessmentDetailsForDraft(id))).then(result => result.flat())
+const apiGetCoursesForDraft = draftId => {
+ return fetch(`/api/courses/${draftId}`, defaultOptions())
+ .then(res => res.json())
+ .then(res => res.value)
+}
+
+const apiGetAssessmentDetailsForCourse = params => {
+ return fetch(
+ `/api/assessments/${params.draftId}/course/${params.contextId}/details`,
+ defaultOptions()
+ )
+ .then(res => res.json())
+ .then(res => res.value)
+}
+
module.exports = {
apiGetAssessmentDetailsForMultipleDrafts,
- apiGetAssessmentDetailsForDraft
+ apiGetAssessmentDetailsForDraft,
+ apiGetCoursesForDraft,
+ apiGetAssessmentDetailsForCourse
}
diff --git a/packages/app/obojobo-repository/shared/actions/shared-api-methods.test.js b/packages/app/obojobo-repository/shared/actions/shared-api-methods.test.js
index f9c82df766..c8ed558005 100644
--- a/packages/app/obojobo-repository/shared/actions/shared-api-methods.test.js
+++ b/packages/app/obojobo-repository/shared/actions/shared-api-methods.test.js
@@ -1,6 +1,8 @@
const {
apiGetAssessmentDetailsForMultipleDrafts,
- apiGetAssessmentDetailsForDraft
+ apiGetAssessmentDetailsForDraft,
+ apiGetCoursesForDraft,
+ apiGetAssessmentDetailsForCourse
} = require('./shared-api-methods')
describe('sharedAPIMethods', () => {
@@ -123,4 +125,43 @@ describe('sharedAPIMethods', () => {
}
])
})
+
+ test('apiGetCoursesForDraft returns expected object', async () => {
+ const result = await apiGetCoursesForDraft('draft-id-1')
+
+ expect(global.fetch).toHaveBeenCalledTimes(1)
+ expect(result).toEqual([
+ {
+ attemptId: 'mock-attempt-1',
+ draftId: 'draft-id-1',
+ userRoles: ['A']
+ },
+ {
+ attemptId: 'mock-attempt-2',
+ draftId: 'draft-id-1',
+ userRoles: ['A']
+ }
+ ])
+ })
+
+ test('apiGetAssessmentDetailsForCourse returns expected object', async () => {
+ const result = await apiGetAssessmentDetailsForCourse({
+ draftId: 'draft-id-1',
+ contextId: 'context-id-1'
+ })
+
+ expect(global.fetch).toHaveBeenCalledTimes(1)
+ expect(result).toEqual([
+ {
+ attemptId: 'mock-attempt-1',
+ draftId: 'draft-id-1',
+ userRoles: ['A']
+ },
+ {
+ attemptId: 'mock-attempt-2',
+ draftId: 'draft-id-1',
+ userRoles: ['A']
+ }
+ ])
+ })
})
diff --git a/packages/app/obojobo-repository/shared/actions/stats-actions.js b/packages/app/obojobo-repository/shared/actions/stats-actions.js
index 1d96acd7b8..83ee967dce 100644
--- a/packages/app/obojobo-repository/shared/actions/stats-actions.js
+++ b/packages/app/obojobo-repository/shared/actions/stats-actions.js
@@ -1,6 +1,9 @@
// =================== API =======================
-const { apiGetAssessmentDetailsForMultipleDrafts } = require('./shared-api-methods')
+const {
+ apiGetAssessmentDetailsForMultipleDrafts,
+ apiGetAssessmentDetailsForCourse
+} = require('./shared-api-methods')
// ================== ACTIONS ===================
@@ -29,9 +32,17 @@ const loadModuleAssessmentDetails = draftIds => ({
promise: apiGetAssessmentDetailsForMultipleDrafts(draftIds)
})
+const LOAD_COURSE_ASSESSMENT_DATA = 'LOAD_COURSE_ASSESSMENT_DATA'
+const loadCourseAssessmentData = params => ({
+ type: LOAD_COURSE_ASSESSMENT_DATA,
+ promise: apiGetAssessmentDetailsForCourse(params)
+})
+
module.exports = {
LOAD_STATS_PAGE_MODULES_FOR_USER,
LOAD_MODULE_ASSESSMENT_DETAILS,
+ LOAD_COURSE_ASSESSMENT_DATA,
loadUserModuleList,
- loadModuleAssessmentDetails
+ loadModuleAssessmentDetails,
+ loadCourseAssessmentData
}
diff --git a/packages/app/obojobo-repository/shared/actions/stats-actions.test.js b/packages/app/obojobo-repository/shared/actions/stats-actions.test.js
index e6fbe6d81d..a0e21f191d 100644
--- a/packages/app/obojobo-repository/shared/actions/stats-actions.test.js
+++ b/packages/app/obojobo-repository/shared/actions/stats-actions.test.js
@@ -1,7 +1,12 @@
-const { loadUserModuleList, loadModuleAssessmentDetails } = require('./stats-actions')
+const {
+ loadUserModuleList,
+ loadModuleAssessmentDetails,
+ loadCourseAssessmentData
+} = require('./stats-actions')
jest.mock('./shared-api-methods', () => ({
- apiGetAssessmentDetailsForMultipleDrafts: () => Promise.resolve()
+ apiGetAssessmentDetailsForMultipleDrafts: () => Promise.resolve(),
+ apiGetAssessmentDetailsForCourse: () => Promise.resolve()
}))
describe('statsActions', () => {
@@ -34,4 +39,16 @@ describe('statsActions', () => {
promise: expect.any(Promise)
})
})
+
+ test('loadCourseAssessmentData returns expected object', async () => {
+ const result = await loadCourseAssessmentData({
+ draftId: 'draft-id-1',
+ contextId: 'context-id-2'
+ })
+
+ expect(result).toEqual({
+ type: 'LOAD_COURSE_ASSESSMENT_DATA',
+ promise: expect.any(Promise)
+ })
+ })
})
diff --git a/packages/app/obojobo-repository/shared/components/__snapshots__/course-score-data-dialog.test.js.snap b/packages/app/obojobo-repository/shared/components/__snapshots__/course-score-data-dialog.test.js.snap
new file mode 100644
index 0000000000..e61a1f4c61
--- /dev/null
+++ b/packages/app/obojobo-repository/shared/components/__snapshots__/course-score-data-dialog.test.js.snap
@@ -0,0 +1,571 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CourseScoreDataDialog CourseMenu Menu renders correctly with courses 1`] = `
+
@@ -292,6 +307,21 @@ exports[`ModuleOptionsDialog renders correctly with Minimal access level 1`] = `
View scores by student.
+
@@ -428,6 +458,21 @@ exports[`ModuleOptionsDialog renders correctly with Partial access level 1`] = `
View scores by student.
+
@@ -628,6 +673,21 @@ exports[`ModuleOptionsDialog renders correctly with standard expected props 1`]
View scores by student.
+
diff --git a/packages/app/obojobo-repository/shared/components/course-score-data-dialog.jsx b/packages/app/obojobo-repository/shared/components/course-score-data-dialog.jsx
new file mode 100644
index 0000000000..18dfe760b8
--- /dev/null
+++ b/packages/app/obojobo-repository/shared/components/course-score-data-dialog.jsx
@@ -0,0 +1,226 @@
+require('./course-score-data-dialog.scss')
+
+const { CSSTransition } = require('react-transition-group')
+const React = require('react')
+const ModuleImage = require('./module-image')
+const Button = require('./button')
+const Loading = require('./loading')
+const ReactModal = require('react-modal')
+const dayjs = require('dayjs')
+const CourseStats = require('./stats/course-stats')
+const CourseScoreDataListItem = require('./course-score-data-list-item')
+const CourseStatsTypeSelect = require('./stats/course-stats-type-select')
+const CourseStatsFilterControls = require('./stats/course-stats-filter-controls')
+const CourseStatsSearchControls = require('./stats/course-stats-search-controls')
+const Search = require('./search')
+
+const VIEW_MODE_FINAL_ASSESSMENT_SCORE = 'final-assessment-scores'
+
+const CourseScoreDataDialog = ({ draftId, title, onClose, isCoursesLoading, courses }) => {
+ // const [fetchUrl, setFetchUrl] = React.useState(null)
+ const [selectedIndex, setSelectedIndex] = React.useState(null)
+ const [selectedCourse, setSelectedCourse] = React.useState(null)
+ const [courseIsLoading, setCourseIsLoading] = React.useState(false)
+ const [courseHasLoaded, setCourseHasLoaded] = React.useState(false)
+ const [courseData, setCourseData] = React.useState(null)
+ const [isMenuOpen, setIsMenuOpen] = React.useState(true)
+ const [courseSearch, setCourseSearch] = React.useState('')
+
+ const [searchViewMode, setSearchViewMode] = React.useState(VIEW_MODE_FINAL_ASSESSMENT_SCORE)
+ const [searchSettings, setSearchSettings] = React.useState('')
+ const [searchContent, setSearchContent] = React.useState('')
+ const [searchFilterSettings, setSearchFilterSettings] = React.useState({
+ showIncompleteAttempts: false,
+ showPreviewAttempts: false,
+ showAdvancedFields: false
+ })
+
+ const baseUrl = `/api/assessments/${draftId}`
+ const menuRef = React.createRef()
+
+ const filterCourses = courses => {
+ if (courseSearch.trim().length == 0) {
+ return courses
+ }
+ // This turns the search string into an array of words.
+ const lowerCaseArray = courseSearch
+ .trim()
+ .toLowerCase()
+ .split(' ')
+ return courses.filter(course => {
+ const haystack = course.contextTitle.toLowerCase() + course.contextLabel.toLowerCase()
+
+ // If each word in the search can be found in the course title and label, return true
+ let containsAll = true
+ lowerCaseArray.forEach(word => {
+ if (!haystack.includes(word)) {
+ containsAll = false
+ }
+ })
+ return containsAll
+ })
+ }
+
+ const filteredCourses = filterCourses(courses)
+
+ const selectCourse = async contextId => {
+ if (selectedCourse == contextId) {
+ return
+ }
+ setCourseData(null)
+ setCourseIsLoading(true)
+ setCourseHasLoaded(false)
+ filteredCourses.forEach(course => {
+ if (course.contextId == contextId) {
+ setSelectedCourse(course)
+ }
+ })
+
+ const newFetchUrl = `${baseUrl}/course/${contextId}/details`
+ setSelectedIndex(contextId)
+ const courseData = await runFetch(newFetchUrl)
+ loadCourseData(courseData)
+ }
+
+ const runFetch = fetchUrl => {
+ return fetch(fetchUrl, {})
+ .then(res => res.json())
+ .then(res => res.value)
+ }
+
+ const loadCourseData = courseData => {
+ setCourseData(courseData)
+ setCourseIsLoading(false)
+ setCourseHasLoaded(true)
+ }
+
+ const toggleMenu = () => {
+ setIsMenuOpen(!isMenuOpen)
+ }
+
+ const renderMenuToggleButton = () => {
+ return (
+
+ )
+ }
+
+ const renderCourseMenu = () => {
+ return (
+
+
+
+
+
Available Courses
+
Recently Accessed First
+
+
+
+ {renderMenuToggleButton()}
+
+ {filteredCourses.length === 0 ? (
+
No Courses Found
+ ) : (
+ ''
+ )}
+ {filteredCourses.map(course => (
+
+ ))}
+
+
{renderMenuToggleButton()}
+
+
+ )
+ }
+
+ const changeViewMode = mode => {
+ setSearchViewMode(mode)
+ }
+
+ const validCourseSelected = selectedIndex !== null
+ const currentCourseTitle =
+ validCourseSelected && selectedCourse
+ ? `${selectedCourse.contextTitle} (${selectedCourse.contextLabel})`
+ : 'Select a Course to View Assessment Data'
+ const currentCourseAccessed =
+ validCourseSelected && selectedCourse
+ ? `Last Accessed ${dayjs(selectedCourse.lastAccessed).format('MMM Do YYYY - h:mm A')}`
+ : ''
+
+ return (
+
+
+
+
+ {renderCourseMenu()}
+
+ {validCourseSelected ? (
+
+
+
+
{currentCourseTitle}
+
{currentCourseAccessed}
+
+
+
+
+
+
+
+
+
+ {courseHasLoaded ? (
+
+ ) : (
+
+ {!courseIsLoading ? '' : 'Loading course data...'}
+
+ )}
+
+ ) : (
+ Select a Course to View Assessment Data
+ )}
+
+
+
+ )
+}
+
+module.exports = CourseScoreDataDialog
diff --git a/packages/app/obojobo-repository/shared/components/course-score-data-dialog.scss b/packages/app/obojobo-repository/shared/components/course-score-data-dialog.scss
new file mode 100644
index 0000000000..4bfca10a0c
--- /dev/null
+++ b/packages/app/obojobo-repository/shared/components/course-score-data-dialog.scss
@@ -0,0 +1,316 @@
+@import '../../client/css/defaults';
+
+$border-color: rgba(0, 0, 0, 0.1);
+$border: 1px solid $border-color;
+$menu-transition-time: 0.25s;
+
+.course-score-data-dialog {
+ font-family: $font-default;
+ width: calc(100vw - 3em);
+
+ .loading--container {
+ height: 100%;
+ position: absolute;
+ top: 50%;
+ width: 100%;
+ text-align: center;
+ }
+}
+
+.course-score-data-dialog--header {
+ height: 3em;
+ box-sizing: border-box;
+ position: relative;
+ border-bottom: $border;
+
+ .repository--module-icon--image {
+ width: 1.5em;
+ height: 1.75em;
+ display: inline-block;
+ vertical-align: middle;
+ position: absolute;
+ left: 0.75em;
+ top: 50%;
+ transform: translate(0, -50%);
+ }
+
+ .title {
+ position: absolute;
+ left: 4em;
+ font-size: 0.725em;
+ font-weight: bold;
+ color: black;
+ margin: 0;
+ max-width: 50%;
+ vertical-align: middle;
+ top: 50%;
+ transform: translate(0, -50%);
+ }
+
+ .close-button {
+ position: absolute;
+ right: 0.5em;
+ top: 50%;
+ transform: translate(0, -50%);
+ }
+}
+
+.course-score-data-dialog--body {
+ background-color: #f2f2f2;
+ display: flex;
+ height: calc(95vh - 3em);
+ width: 100%;
+ max-width: 100%;
+
+ span {
+ display: block;
+ }
+}
+
+.text-loader {
+ width: 100%;
+ text-align: center;
+ padding: 24px;
+}
+
+.no-courses-text {
+ text-align: center;
+ padding: 24px;
+}
+
+.course-score-data-list {
+ $open-width-max: 20em;
+ $open-width-min: 15em;
+ $closed-width: 1.5em;
+
+ max-width: $open-width-max;
+ min-width: $open-width-min;
+ background-color: white;
+ border-right: $border;
+ overflow-y: auto;
+ overflow-x: hidden;
+ transition: min-width $menu-transition-time, max-width $menu-transition-time;
+
+ .menu-collapsed {
+ display: none;
+ position: relative;
+ height: 100%;
+ background-color: white;
+ padding-top: 0.9em;
+ }
+
+ .toggle-button {
+ background-image: url('../../../obojobo-document-engine/src/scripts/viewer/components/arrow.svg');
+ background-color: transparent;
+ background-position: center center;
+ background-repeat: no-repeat;
+ border: none;
+ cursor: pointer;
+ height: 1.32rem;
+ overflow: hidden;
+ text-indent: -9999px;
+ transition: background-color $menu-transition-time;
+ transition: transform $menu-transition-time;
+ width: 1.6rem;
+ margin-right: 0.5em;
+ position: absolute;
+ top: 1em;
+ right: 0;
+ }
+
+ // Classes used by CSSTransition when the
+ // menu is collapsed/expanded
+ &.exit-done,
+ &.enter-active {
+ overflow-y: hidden;
+
+ .menu-expanded {
+ display: none;
+ }
+
+ .menu-collapsed {
+ display: block;
+ }
+
+ .toggle-button {
+ transform: rotate(180deg);
+ float: none;
+ margin: 0 auto;
+ display: block;
+
+ &:not(:hover) {
+ background-image: url('../../../obojobo-document-engine/src/scripts/viewer/components/hamburger.svg');
+ }
+ }
+ }
+
+ &.enter-active {
+ min-width: 0;
+ max-width: 0;
+ }
+
+ &.exit {
+ min-width: 0;
+ max-width: 0;
+ }
+
+ &.exit-done {
+ min-width: $closed-width;
+ max-width: $closed-width;
+ }
+
+ .course-score-data-list--title {
+ white-space: nowrap; // prevent wrapping during animation
+ padding: 0.9em 0 0.9em 1.5em;
+ font-weight: bold;
+ font-size: 0.8em;
+ width: 100%;
+ box-sizing: border-box;
+ background-color: white;
+ position: sticky;
+ top: 0;
+ border-bottom: $border;
+
+ .desc {
+ font-weight: 300;
+ font-size: 0.8em;
+ font-style: italic;
+ color: #6f6e6e;
+ }
+
+ .course-search {
+ padding-right: 1.5em;
+ padding-top: 0.5em;
+ font-size: 1.25em;
+ }
+ }
+
+ .course-score-data-list--item {
+ border-bottom: $border;
+ padding: 1em 1.5em;
+ background-color: white;
+ font-size: 0.75em;
+ min-width: 14em;
+ cursor: pointer;
+
+ .courseTitle {
+ margin-bottom: 0.4em;
+ font-weight: bold;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ .userCount {
+ color: $color-text-minor;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ margin-bottom: 0.2em;
+ }
+
+ .accessed {
+ color: #0d4fa7;
+ margin-top: 0.4em;
+ font-size: 0.8em;
+ white-space: nowrap;
+
+ .timestamp {
+ display: inline;
+ background-color: lighten($color-nav-highlight, 55%);
+ color: $color-nav-highlight;
+ text-align: center;
+ border-radius: 5em;
+ margin-top: 0.6em;
+ padding: 0.2em 0.75em;
+ }
+ }
+
+ &.is-selected {
+ background-image: url('./hex_transparent.svg');
+ background-repeat: no-repeat;
+ background-origin: content-box;
+ background-position-x: 100%;
+ background-color: $color-nav-bg-selected;
+
+ .courseTitle {
+ color: $color-nav-highlight;
+ }
+ }
+
+ &:hover {
+ background-color: $color-nav-bg-hover;
+
+ .courseTitle {
+ color: $color-nav-hover;
+ }
+ }
+ }
+}
+
+.data-viewer {
+ flex: 4;
+ padding: 1em;
+ justify-content: center;
+ align-items: center;
+ background-color: #f2f2f2;
+ overflow-x: auto;
+
+ .data-viewer--header {
+ padding: 0.8em;
+ border: $border;
+ background-color: $color-nav-bg-selected;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 104px;
+ box-sizing: border-box;
+ overflow-y: hidden;
+
+ .data-viewer--header--title-container {
+ display: flex;
+ flex-grow: 1;
+ flex-shrink: 1;
+ flex-direction: column;
+ justify-content: space-between;
+
+ .data-viewer--header--title {
+ font-weight: bold;
+ }
+
+ small {
+ display: block;
+ color: $color-text-minor;
+ margin-top: 0.5em;
+ font-size: 0.7em;
+ }
+ }
+
+ .data-viewer--header--filter-container {
+ font-size: 0.75em;
+ }
+ }
+
+ .data-viewer--filter-controls {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 0.5em 16px;
+ box-sizing: border-box;
+ background-color: white;
+ border: 1px solid $border-color;
+ border-top: none;
+ height: 46px;
+ max-height: 46px;
+
+ .view-mode {
+ font-size: 0.8em;
+
+ select {
+ font-size: 1.25em;
+ margin-left: 0.5em;
+ }
+ }
+ }
+}
diff --git a/packages/app/obojobo-repository/shared/components/course-score-data-dialog.test.js b/packages/app/obojobo-repository/shared/components/course-score-data-dialog.test.js
new file mode 100644
index 0000000000..c4ee3293ac
--- /dev/null
+++ b/packages/app/obojobo-repository/shared/components/course-score-data-dialog.test.js
@@ -0,0 +1,382 @@
+import React from 'react'
+import Loading from './loading'
+import { act, create } from 'react-test-renderer'
+import CourseScoreDataDialog from './course-score-data-dialog'
+import CourseScoreDataListItem from './course-score-data-list-item'
+import CourseStatsTypeSelect from './stats/course-stats-type-select'
+
+const VIEW_MODE_FINAL_ASSESSMENT_SCORE = 'final-assessment-scores'
+const VIEW_MODE_ALL_ATTEMPTS = 'all-attempts'
+
+jest.mock('./stats/course-stats', () => props => {
+ return
+})
+
+jest.mock('react-transition-group', () => ({
+ // eslint-disable-next-line react/display-name
+ CSSTransition: props =>
{props.children}
+}))
+
+describe('CourseScoreDataDialog', () => {
+ const originalFetch = global.fetch
+
+ const dialogProps = () => ({
+ draftId: 'mock-draft-id',
+ title: 'mock-title',
+ onClose: jest.fn(),
+ isCoursesLoading: true,
+ courses: []
+ })
+
+ const courses = [
+ {
+ contextTitle: 'Mock Course 1',
+ contextLabel: 'mock-course-1',
+ userCount: '10',
+ lastAccessed: '2023-06-21 13:33:03.34966+00',
+ contextId: 'S3294471'
+ },
+ {
+ contextTitle: 'Mock Course 2',
+ contextLabel: 'mock-course-2',
+ userCount: '200',
+ lastAccessed: '2023-06-22 14:33:03.34966+00',
+ contextId: 'S3294472'
+ },
+ {
+ contextTitle: 'Mock Course 3',
+ contextLabel: 'mock-course-3',
+ userCount: '3000',
+ lastAccessed: '2023-06-23 15:33:03.34966+00',
+ contextId: 'S3294473'
+ }
+ ]
+
+ beforeEach(() => {
+ jest.resetAllMocks()
+
+ global.fetch = jest.fn()
+ })
+
+ afterAll(() => {
+ global.fetch = originalFetch
+ })
+
+ test('Modal renders correctly when history is loading', () => {
+ const component = create(
)
+
+ const tree = component.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ test('Modal renders correctly when no course data', () => {
+ const component = create(
)
+
+ const tree = component.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ test('Modal renders correctly with courses', () => {
+ const component = create(
+
+ )
+
+ const tree = component.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ test('Modal - clicking the close button calls onClose', () => {
+ const onClose = jest.fn()
+ const component = create(
)
+
+ expect(onClose).not.toHaveBeenCalled()
+ component.root
+ .findByProps({ className: 'close-button' })
+ .props.onClick({ target: 'mock-target' })
+ expect(onClose).toHaveBeenCalledWith({ target: 'mock-target' })
+ })
+
+ describe('CourseMenu', () => {
+ beforeEach(() => {
+ jest.resetAllMocks()
+ })
+
+ const assignments = [
+ {
+ user_username: 'sis:tstlearner15',
+ user_first_name: 'Test4',
+ user_last_name: 'Learner4',
+ user_roles: '{Learner}',
+ assessment_score_id: 13,
+ user_id: 15,
+ draft_id: 'ff85b3d8-a6c8-4446-a25a-bf0c16d5a8b9',
+ assessment_id: 'my-assessment',
+ attempt_id: '33d4335b-48fb-4897-aedd-7e6abc17e3fb',
+ assessment_score: 100,
+ is_preview: false,
+ assessment_score_details: {
+ status: 'passed',
+ rewardTotal: 0,
+ attemptScore: 100,
+ rewardedMods: [],
+ attemptNumber: 1,
+ assessmentScore: 100,
+ assessmentModdedScore: 100
+ },
+ draft_content_id: '8d108c19-4f67-4237-a78d-c9827af4053e',
+ is_imported: false,
+ imported_assessment_score_id: null,
+ attempt_state: {
+ chosen: [
+ {
+ id: 'e32101dd-8f0e-4bec-b519-968270efb426',
+ type: 'ObojoboDraft.Chunks.Question'
+ },
+ {
+ id: 'f1ebeb0e-606f-4d1e-b8b9-2ba4fbfbfa0f',
+ type: 'ObojoboDraft.Chunks.Question'
+ },
+ {
+ id: 'fa4c4db0-30f1-4386-9ce9-956aaf228378',
+ type: 'ObojoboDraft.Chunks.Question'
+ },
+ {
+ id: '5345c7c6-ac69-4e9f-80da-6037765fa612',
+ type: 'ObojoboDraft.Chunks.QuestionBank'
+ },
+ {
+ id: 'af4ea08a-e488-4156-a2a0-c0f9a64c58eb',
+ type: 'ObojoboDraft.Chunks.QuestionBank'
+ }
+ ]
+ },
+ attempt_result: {
+ attemptScore: 100,
+ questionScores: [
+ {
+ id: 'e32101dd-8f0e-4bec-b519-968270efb426',
+ score: 100
+ },
+ {
+ id: 'f1ebeb0e-606f-4d1e-b8b9-2ba4fbfbfa0f',
+ score: 100
+ },
+ {
+ id: 'fa4c4db0-30f1-4386-9ce9-956aaf228378',
+ score: 100
+ }
+ ]
+ },
+ created_at: '2023-06-20 14:32:55.07198+00',
+ completed_at: '2023-06-20 14:33:03.34966+00',
+ resource_link_id: 'course_3',
+ imported_attempt_id: null,
+ lti_status: 'success',
+ lti_status_details: null,
+ lti_gradebook_status: 'ok_gradebook_matches_assessment_score',
+ lti_score_sent: 1,
+ context_id: 'S3294478',
+ course_title: 'Obojobo Local Dev 103',
+ resource_link_title: null,
+ launch_presentation_return_url: null,
+ module_title: 'Obojobo Example Document Copy'
+ }
+ ]
+
+ test('Menu renders correctly with courses', () => {
+ const component = create(
+
+ )
+
+ expect(
+ component.root.findAllByProps({ className: 'course-score-data-list--item' }).length
+ ).toBe(3)
+ const tree = component.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ test('Menu filters correctly with text that matches all items', () => {
+ const component = create(
+
+ )
+
+ const courseSearch = component.root
+ .findAllByProps({ className: 'course-search' })[0]
+ .findByType('input')
+ // execute
+ act(() => {
+ const mockChangeEvent = { target: { value: 'mock' } }
+ courseSearch.props.onChange(mockChangeEvent)
+ })
+ expect(
+ component.root.findAllByProps({ className: 'course-score-data-list--item' }).length
+ ).toBe(3)
+ })
+
+ test('Menu filters correctly with text that matches no items', () => {
+ const component = create(
+
+ )
+
+ const courseSearch = component.root
+ .findAllByProps({ className: 'course-search' })[0]
+ .findByType('input')
+ // execute
+ act(() => {
+ const mockChangeEvent = { target: { value: 'unused-mock' } }
+ courseSearch.props.onChange(mockChangeEvent)
+ })
+ expect(
+ component.root.findAllByProps({ className: 'course-score-data-list--item' }).length
+ ).toBe(0)
+ })
+
+ test('Menu filters correctly with text that matches some items', () => {
+ const component = create(
+
+ )
+
+ const courseSearch = component.root
+ .findAllByProps({ className: 'course-search' })[0]
+ .findByType('input')
+ // execute
+ act(() => {
+ const mockChangeEvent = { target: { value: 'mock-course-3' } }
+ courseSearch.props.onChange(mockChangeEvent)
+ })
+ expect(
+ component.root.findAllByProps({ className: 'course-score-data-list--item' }).length
+ ).toBe(1)
+ })
+
+ test('Menu hides and expands correctly', () => {
+ const componentTemplate = (
+
+ )
+
+ let component
+ act(() => {
+ component = create(componentTemplate)
+ })
+
+ // The menu starts out open (prop: in == true)
+ expect(component.root.findByType(Loading).children[0].props.in).toBe(true)
+
+ // component.root.toggleMenu = jest.fn()
+ // const toggleButtons = component.root.findAllByProps({ className: 'toggle-button' })
+ // toggleButtons[0].props.onClick()
+ const courseList = component.root.findByProps({ className: 'course-score-data-list' })
+ const toggleButtons = courseList.findAllByType('button')
+
+ act(() => {
+ toggleButtons[0].props.onClick()
+ component.update(componentTemplate)
+ })
+ expect(component.root.findByType(Loading).children[0].props.in).toBe(false)
+
+ // Click again to open (prop: in == true)
+ act(() => {
+ toggleButtons[1].props.onClick()
+ component.update(componentTemplate)
+ })
+ expect(component.root.findByType(Loading).children[0].props.in).toBe(true)
+ })
+
+ test('Menu selects course when clicked on', async () => {
+ const mockFetch = jest.fn(() =>
+ Promise.resolve({
+ json: () => Promise.resolve({ value: assignments })
+ })
+ )
+ global.fetch = mockFetch
+
+ const component = create(
+
+ )
+
+ const course1Button = component.root.findAllByType(CourseScoreDataListItem)[0]
+ const course2Button = component.root.findAllByType(CourseScoreDataListItem)[1]
+ const course3Button = component.root.findAllByType(CourseScoreDataListItem)[2]
+ const course1ButtonIndex = course1Button.props.index
+ const course2ButtonIndex = course2Button.props.index
+ const course3ButtonIndex = course3Button.props.index
+
+ // Initially, no buttons are selected
+ expect(course1Button.props.isSelected).toBe(false)
+ expect(course2Button.props.isSelected).toBe(false)
+ expect(course3Button.props.isSelected).toBe(false)
+
+ // Test if nothing is sent: this is to test for if(selectedCourse == contextId)
+ await act(async () => {
+ await course3Button.props.courseClick(null)
+ })
+ expect(course1Button.props.isSelected).toBe(false)
+ expect(course2Button.props.isSelected).toBe(false)
+ expect(course3Button.props.isSelected).toBe(false)
+
+ // Click the first course, and it should be selected
+ await act(async () => {
+ await course1Button.props.courseClick(course1ButtonIndex)
+ })
+ expect(course1Button.props.isSelected).toBe(true)
+ expect(course2Button.props.isSelected).toBe(false)
+ expect(course3Button.props.isSelected).toBe(false)
+
+ // Click the second course, and it should be selected and the first should deselect.
+ await act(async () => {
+ await course2Button.props.courseClick(course2ButtonIndex)
+ })
+ expect(course1Button.props.isSelected).toBe(false)
+ expect(course2Button.props.isSelected).toBe(true)
+ expect(course3Button.props.isSelected).toBe(false)
+
+ // Click the second course AGAIN, and it should stay selected.
+ await act(async () => {
+ await course2Button.props.courseClick(course2ButtonIndex)
+ })
+ expect(course1Button.props.isSelected).toBe(false)
+ expect(course2Button.props.isSelected).toBe(true)
+ expect(course3Button.props.isSelected).toBe(false)
+
+ // Click the third course, and it should be selected
+ await act(async () => {
+ await course3Button.props.courseClick(course3ButtonIndex)
+ })
+ expect(course1Button.props.isSelected).toBe(false)
+ expect(course2Button.props.isSelected).toBe(false)
+ expect(course3Button.props.isSelected).toBe(true)
+ })
+
+ test('Changing the view type is caught and saved', async () => {
+ const mockFetch = jest.fn(() =>
+ Promise.resolve({
+ json: () => Promise.resolve({ value: assignments })
+ })
+ )
+ global.fetch = mockFetch
+
+ const component = create(
+
+ )
+
+ const course1Button = component.root.findAllByType(CourseScoreDataListItem)[0]
+ const course1ButtonIndex = course1Button.props.index
+
+ // Initially, no buttons are selected
+ expect(course1Button.props.isSelected).toBe(false)
+
+ // Click the first course, and it should be selected
+ await act(async () => {
+ await course1Button.props.courseClick(course1ButtonIndex)
+ })
+ expect(course1Button.props.isSelected).toBe(true)
+
+ const typeSelector = component.root.findByType(CourseStatsTypeSelect)
+ expect(typeSelector.props.viewMode).toBe(VIEW_MODE_FINAL_ASSESSMENT_SCORE)
+ act(() => {
+ typeSelector.props.onChangeViewMode(VIEW_MODE_ALL_ATTEMPTS)
+ })
+ expect(typeSelector.props.viewMode).toBe(VIEW_MODE_ALL_ATTEMPTS)
+ })
+ })
+})
diff --git a/packages/app/obojobo-repository/shared/components/course-score-data-list-item.jsx b/packages/app/obojobo-repository/shared/components/course-score-data-list-item.jsx
new file mode 100644
index 0000000000..8e903f0eb3
--- /dev/null
+++ b/packages/app/obojobo-repository/shared/components/course-score-data-list-item.jsx
@@ -0,0 +1,43 @@
+const React = require('react')
+
+// This component is the clickable list item for the "Course Stats" dialog. Each instance is a different available course.
+const CourseScoreDataListItem = ({
+ courseTitle,
+ courseLabel,
+ courseUserCount,
+ courseLastAccessed,
+ courseClick,
+ isSelected,
+ index
+}) => {
+ const onClickHandler = React.useCallback(() => {
+ courseClick(index)
+ }, [index])
+
+ const className = 'course-score-data-list--item' + (isSelected ? ' is-selected' : '')
+
+ const titleAndLabel = (
+
+ {courseTitle} ({courseLabel})
+
+ )
+ const userCount = (
+
+ {courseUserCount} Learner{courseUserCount === 1 ? '' : 's'}
+
+ )
+ const accessed = (
+
+ Last Accessed {courseLastAccessed}
+
+ )
+ return (
+
+ {titleAndLabel}
+ {userCount}
+ {accessed}
+
+ )
+}
+
+module.exports = CourseScoreDataListItem
diff --git a/packages/app/obojobo-repository/shared/components/course-score-data-list-item.test.js b/packages/app/obojobo-repository/shared/components/course-score-data-list-item.test.js
new file mode 100644
index 0000000000..2453a6cb88
--- /dev/null
+++ b/packages/app/obojobo-repository/shared/components/course-score-data-list-item.test.js
@@ -0,0 +1,45 @@
+import React from 'react'
+import { create } from 'react-test-renderer'
+
+import CourseScoreDataListItem from './course-score-data-list-item'
+
+describe('CourseScoreDataListItem', () => {
+ const standardProps = {
+ courseTitle: 'Mock Course Title',
+ courseLabel: 'MCT-101',
+ courseUserCount: 10,
+ courseLastAccessed: 'Apr 1st 2020 - 9:15 AM',
+ courseClick: jest.fn(),
+ isSelected: false,
+ index: 3
+ }
+
+ test('renders with standard expected props', () => {
+ const component = create(
)
+
+ expect(component.toJSON()).toMatchSnapshot()
+ })
+
+ test('renders correctly when selected', () => {
+ const component = create(
)
+
+ expect(component.toJSON()).toMatchSnapshot()
+ })
+
+ test('user count displays properly without the "s" for 1 learner', () => {
+ standardProps.courseUserCount = 1
+
+ const component = create(
)
+
+ expect(component.toJSON()).toMatchSnapshot()
+ })
+
+ test('onClick callback is called properly', () => {
+ const component = create(
)
+
+ component.root.findByProps({ className: 'course-score-data-list--item' }).props.onClick()
+
+ expect(standardProps.courseClick).toHaveBeenCalledTimes(1)
+ expect(standardProps.courseClick).toHaveBeenCalledWith(standardProps.index)
+ })
+})
diff --git a/packages/app/obojobo-repository/shared/components/dashboard-hoc.js b/packages/app/obojobo-repository/shared/components/dashboard-hoc.js
index a54ef88455..0f0a58138c 100644
--- a/packages/app/obojobo-repository/shared/components/dashboard-hoc.js
+++ b/packages/app/obojobo-repository/shared/components/dashboard-hoc.js
@@ -31,6 +31,7 @@ const {
moduleRemoveFromCollection,
showVersionHistory,
showAssessmentScoreData,
+ showCoursesByDraft,
restoreVersion,
importModuleFile,
checkModuleLock,
@@ -70,6 +71,7 @@ const mapActionsToProps = {
moduleRemoveFromCollection,
showVersionHistory,
showAssessmentScoreData,
+ showCoursesByDraft,
restoreVersion,
importModuleFile,
checkModuleLock,
diff --git a/packages/app/obojobo-repository/shared/components/dashboard-hoc.test.js b/packages/app/obojobo-repository/shared/components/dashboard-hoc.test.js
index 65bbcf9a53..68b99eeb77 100644
--- a/packages/app/obojobo-repository/shared/components/dashboard-hoc.test.js
+++ b/packages/app/obojobo-repository/shared/components/dashboard-hoc.test.js
@@ -42,6 +42,7 @@ describe('Dashboard HOC', () => {
deselectModules: DashboardActions.deselectModules,
selectModules: DashboardActions.selectModules,
showAssessmentScoreData: DashboardActions.showAssessmentScoreData,
+ showCoursesByDraft: DashboardActions.showCoursesByDraft,
filterModules: DashboardActions.filterModules,
importModuleFile: DashboardActions.importModuleFile,
filterCollections: DashboardActions.filterCollections,
diff --git a/packages/app/obojobo-repository/shared/components/dashboard.jsx b/packages/app/obojobo-repository/shared/components/dashboard.jsx
index 5001ae824b..d973d2fa46 100644
--- a/packages/app/obojobo-repository/shared/components/dashboard.jsx
+++ b/packages/app/obojobo-repository/shared/components/dashboard.jsx
@@ -14,6 +14,7 @@ const MultiButton = require('./multi-button')
const Search = require('./search')
const ReactModal = require('react-modal')
const AssessmentScoreDataDialog = require('./assessment-score-data-dialog')
+const CourseScoreDataDialog = require('./course-score-data-dialog')
const Spinner = require('./spinner')
const Collection = require('./collection')
const ModuleManageCollectionsDialog = require('./module-manage-collections-dialog')
@@ -37,6 +38,8 @@ const renderOptionsDialog = (props, extension) => (
onClose={props.closeModal}
showVersionHistory={props.showVersionHistory}
showAssessmentScoreData={props.showAssessmentScoreData}
+ showCoursesByDraft={props.showCoursesByDraft}
+ showCourseAssessmentData={props.showCourseAssessmentData}
startLoadingAnimation={props.startLoadingAnimation}
stopLoadingAnimation={props.stopLoadingAnimation}
showModuleManageCollections={props.showModuleManageCollections}
@@ -84,6 +87,18 @@ const renderAssessmentScoreDataDialog = props => {
)
}
+const renderCourseScoreDataDialog = props => {
+ return (
+
+ )
+}
+
const renderModalDialog = props => {
let dialog
let title
@@ -155,6 +170,11 @@ const renderModalDialog = props => {
dialog = renderAssessmentScoreDataDialog(props)
break
+ case 'module-course-score-data':
+ title = 'Module Course Score Data'
+ dialog = renderCourseScoreDataDialog(props)
+ break
+
case 'module-manage-collections':
title = 'Module Collections'
dialog = renderModuleManageCollectionsDialog(props, extendedProps)
diff --git a/packages/app/obojobo-repository/shared/components/dashboard.test.js b/packages/app/obojobo-repository/shared/components/dashboard.test.js
index 1a1801955d..2e617b23cc 100644
--- a/packages/app/obojobo-repository/shared/components/dashboard.test.js
+++ b/packages/app/obojobo-repository/shared/components/dashboard.test.js
@@ -52,6 +52,9 @@ jest.mock('./module-options-dialog', () => props => {
jest.mock('./assessment-score-data-dialog', () => props => {
return
})
+jest.mock('./course-score-data-dialog', () => props => {
+ return
+})
import React from 'react'
import { create, act } from 'react-test-renderer'
@@ -73,6 +76,7 @@ import ModulePermissionsDialog from './module-permissions-dialog'
import ModuleOptionsDialog from './module-options-dialog'
import VersionHistoryDialog from './version-history-dialog'
import AssessmentScoreDataDialog from './assessment-score-data-dialog'
+import CourseScoreDataDialog from './course-score-data-dialog'
import MessageDialog from './message-dialog'
const { MODE_RECENT, MODE_ALL, MODE_COLLECTION, MODE_DELETED } = require('../repository-constants')
@@ -227,6 +231,11 @@ describe('Dashboard', () => {
hasFetched: false,
items: []
},
+ courses: {
+ isFetching: false,
+ hasFetched: false,
+ items: []
+ },
closeModal: jest.fn(),
getModules: jest.fn(),
getDeletedModules: jest.fn(),
@@ -510,7 +519,7 @@ describe('Dashboard', () => {
const component = create(
)
// there shouldn't ever be a case where 'mode' is missing
- // but the default case is equivalent to MODE_RECENT
+ // but the default case is equivalent to MODE_RECENT
expectModeRecentRender(component)
expect(component.toJSON()).toMatchSnapshot()
@@ -1836,6 +1845,25 @@ describe('Dashboard', () => {
component.unmount()
})
+ test('renders "Course Scores" dialog and runs callbacks properly', () => {
+ dashboardProps.dialog = 'module-course-score-data'
+ dashboardProps.selectedModule.title = 'Mock Module Title'
+
+ let component
+ act(() => {
+ component = create(
)
+ })
+
+ expectDialogToBeRendered(component, CourseScoreDataDialog, 'Module Course Score Data')
+ const dialogComponent = component.root.findByType(CourseScoreDataDialog)
+ expect(dialogComponent.props.title).toBe('Assessment Scores by Course')
+
+ dialogComponent.props.onClose()
+ expect(dashboardProps.closeModal).toHaveBeenCalledTimes(1)
+
+ component.unmount()
+ })
+
test('renders "Module Collections" dialog and adjusts callbacks for each mode', async () => {
dashboardProps.dialog = 'module-manage-collections'
dashboardProps.loadModuleCollections = jest.fn()
diff --git a/packages/app/obojobo-repository/shared/components/module-options-dialog.jsx b/packages/app/obojobo-repository/shared/components/module-options-dialog.jsx
index 57c1bb2a99..1e26aeb81b 100644
--- a/packages/app/obojobo-repository/shared/components/module-options-dialog.jsx
+++ b/packages/app/obojobo-repository/shared/components/module-options-dialog.jsx
@@ -79,6 +79,18 @@ const ModuleOptionsDialog = props => (
View scores by student.
+