diff --git a/spec/javascripts/jsx/gradebook/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRendererSpec.js b/spec/javascripts/jsx/gradebook/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRendererSpec.js
index 48680c5c071..c3ba3c7c98d 100644
--- a/spec/javascripts/jsx/gradebook/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRendererSpec.js
+++ b/spec/javascripts/jsx/gradebook/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRendererSpec.js
@@ -21,7 +21,62 @@ import {
createGradebook,
setFixtureHtml
} from 'ui/features/gradebook/react/default_gradebook/__tests__/GradebookSpecHelper.js'
+import StudentColumnHeader from 'ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeader'
import StudentColumnHeaderRenderer from 'ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRenderer.js'
+import StudentLastNameColumnHeader from 'ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentLastNameColumnHeader'
+
+QUnit.module('GradebookGrid StudentLastNameColumnHeaderRenderer', suiteHooks => {
+ let $container
+ let gradebook
+ let renderer
+ let component
+
+ function render() {
+ renderer.render(
+ {} /* column */,
+ $container,
+ {} /* gridSupport */,
+ {
+ ref(ref) {
+ component = ref
+ }
+ }
+ )
+ }
+
+ suiteHooks.beforeEach(() => {
+ $container = document.createElement('div')
+ document.body.appendChild($container)
+ setFixtureHtml($container)
+
+ gradebook = createGradebook({
+ login_handle_name: 'a_jones',
+ sis_name: 'Example SIS'
+ })
+ sinon.stub(gradebook, 'saveSettings')
+ renderer = new StudentColumnHeaderRenderer(gradebook, StudentLastNameColumnHeader, 'student_lastname')
+ })
+
+ suiteHooks.afterEach(() => {
+ $container.remove()
+ })
+
+ QUnit.module('#render()', () => {
+ test('renders the StudentLastNameColumnHeader to the given container node', () => {
+ render()
+ ok($container.innerText.includes('Student Last Name'), 'the "Student Last Name" header is rendered')
+ })
+ })
+
+ QUnit.module('#destroy()', () => {
+ test('unmounts the component', () => {
+ render()
+ renderer.destroy({}, $container)
+ const removed = ReactDOM.unmountComponentAtNode($container)
+ strictEqual(removed, false, 'the component was already unmounted')
+ })
+ })
+})
/* eslint-disable qunit/no-identical-names */
QUnit.module('GradebookGrid StudentColumnHeaderRenderer', suiteHooks => {
@@ -53,7 +108,7 @@ QUnit.module('GradebookGrid StudentColumnHeaderRenderer', suiteHooks => {
sis_name: 'Example SIS'
})
sinon.stub(gradebook, 'saveSettings')
- renderer = new StudentColumnHeaderRenderer(gradebook)
+ renderer = new StudentColumnHeaderRenderer(gradebook, StudentColumnHeader, 'student')
})
suiteHooks.afterEach(() => {
diff --git a/spec/javascripts/jsx/gradebook/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeaderRendererSpec.js b/spec/javascripts/jsx/gradebook/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeaderRendererSpec.js
new file mode 100644
index 00000000000..1af7000260c
--- /dev/null
+++ b/spec/javascripts/jsx/gradebook/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeaderRendererSpec.js
@@ -0,0 +1,135 @@
+/*
+ * 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 .
+ */
+
+import ReactDOM from 'react-dom'
+import {
+ createGradebook,
+ setFixtureHtml
+} from 'ui/features/gradebook/react/default_gradebook/__tests__/GradebookSpecHelper'
+import StudentFirstNameColumnHeader from 'ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeader'
+import StudentFirstNameColumnHeaderRenderer from 'ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeaderRenderer'
+
+/* eslint-disable qunit/no-identical-names */
+QUnit.module('GradebookGrid StudentFirstNameColumnHeaderRenderer', suiteHooks => {
+ let $container
+ let gradebook
+ let renderer
+ let component
+
+ function render() {
+ renderer.render(
+ {} /* column */,
+ $container,
+ {} /* gridSupport */,
+ {
+ ref(ref) {
+ component = ref
+ }
+ }
+ )
+ }
+
+ suiteHooks.beforeEach(() => {
+ $container = document.createElement('div')
+ document.body.appendChild($container)
+ setFixtureHtml($container)
+
+ gradebook = createGradebook({
+ login_handle_name: 'a_jones',
+ sis_name: 'Example SIS'
+ })
+ sinon.stub(gradebook, 'saveSettings')
+ renderer = new StudentFirstNameColumnHeaderRenderer(gradebook)
+ })
+
+ suiteHooks.afterEach(() => {
+ $container.remove()
+ })
+
+ QUnit.module('#render()', () => {
+ test('renders the StudentFirstNameColumnHeader to the given container node', () => {
+ render()
+ ok($container.innerText.includes('Student First Name'), 'the "Student First Name" header is rendered')
+ })
+
+ test('calls the "ref" callback option with the component reference', () => {
+ render()
+ equal(component.constructor.name, 'StudentFirstNameColumnHeader')
+ })
+
+ test('includes a callback for adding elements to the Gradebook KeyboardNav', () => {
+ sinon.stub(gradebook.keyboardNav, 'addGradebookElement')
+ render()
+ component.props.addGradebookElement()
+ strictEqual(gradebook.keyboardNav.addGradebookElement.callCount, 1)
+ })
+
+ test('sets the component as disabled when students are not loaded', () => {
+ gradebook.setStudentsLoaded(false)
+ render()
+ strictEqual(component.props.disabled, true)
+ })
+
+ test('sets the component as not disabled when students are loaded', () => {
+ gradebook.setStudentsLoaded(true)
+ render()
+ strictEqual(component.props.disabled, false)
+ })
+
+ test('includes a callback for keyDown events', () => {
+ sinon.stub(gradebook, 'handleHeaderKeyDown')
+ render()
+ component.props.onHeaderKeyDown({})
+ strictEqual(gradebook.handleHeaderKeyDown.callCount, 1)
+ })
+
+ test('calls Gradebook#handleHeaderKeyDown with a given event', () => {
+ const exampleEvent = new Event('example')
+ sinon.stub(gradebook, 'handleHeaderKeyDown')
+ render()
+ component.props.onHeaderKeyDown(exampleEvent)
+ const event = gradebook.handleHeaderKeyDown.lastCall.args[0]
+ equal(event, exampleEvent)
+ })
+
+ test('calls Gradebook#handleHeaderKeyDown with a given event', () => {
+ sinon.stub(gradebook, 'handleHeaderKeyDown')
+ render()
+ component.props.onHeaderKeyDown({})
+ const columnId = gradebook.handleHeaderKeyDown.lastCall.args[1]
+ equal(columnId, 'student_firstname')
+ })
+
+ test('includes a callback for removing elements to the Gradebook KeyboardNav', () => {
+ sinon.stub(gradebook.keyboardNav, 'removeGradebookElement')
+ render()
+ component.props.removeGradebookElement()
+ strictEqual(gradebook.keyboardNav.removeGradebookElement.callCount, 1)
+ })
+ })
+
+ QUnit.module('#destroy()', () => {
+ test('unmounts the component', () => {
+ render()
+ renderer.destroy({}, $container)
+ const removed = ReactDOM.unmountComponentAtNode($container)
+ strictEqual(removed, false, 'the component was already unmounted')
+ })
+ })
+})
+/* eslint-enable qunit/no-identical-names */
diff --git a/ui/features/gradebook/react/default_gradebook/Gradebook.js b/ui/features/gradebook/react/default_gradebook/Gradebook.js
index 3bc3fbd4176..c5d26f6bece 100644
--- a/ui/features/gradebook/react/default_gradebook/Gradebook.js
+++ b/ui/features/gradebook/react/default_gradebook/Gradebook.js
@@ -321,6 +321,7 @@ class Gradebook extends React.Component {
this.removeHeaderComponentRef = this.removeHeaderComponentRef.bind(this)
// # React Grid Component Rendering Methods
this.updateColumnHeaders = this.updateColumnHeaders.bind(this)
+ this.updateStudentColumnHeaders = this.updateStudentColumnHeaders.bind(this)
// Column Header Helpers
this.handleHeaderKeyDown = this.handleHeaderKeyDown.bind(this)
// Total Grade Column Header
@@ -2233,9 +2234,14 @@ class Gradebook extends React.Component {
setVisibleGridColumns() {
let assignmentGroupId, ref1
- const parentColumnIds = this.gridData.columns.frozen.filter(function (columnId) {
- return !/^custom_col_/.test(columnId)
+ let parentColumnIds = this.gridData.columns.frozen.filter(function (columnId) {
+ return !/^custom_col_/.test(columnId) && !/^student/.test(columnId)
})
+ if (this.gridDisplaySettings.showSeparateFirstLastNames) {
+ parentColumnIds = ['student_lastname', 'student_firstname'].concat(parentColumnIds)
+ } else {
+ parentColumnIds = ['student'].concat(parentColumnIds)
+ }
const customColumnIds = this.listVisibleCustomColumns().map(column => {
return getCustomColumnId(column.id)
})
@@ -2276,18 +2282,14 @@ class Gradebook extends React.Component {
// # Grid Column Definitions
- // Student Column
- buildStudentColumn() {
- let studentColumnWidth
- studentColumnWidth = 150
- if (this.gradebookColumnSizeSettings) {
- if (this.gradebookColumnSizeSettings.student) {
- studentColumnWidth = parseInt(this.gradebookColumnSizeSettings.student, 10)
- }
- }
+ // Student Columns
+ buildStudentColumn(columnId, gradebookColumnSizeSetting, defaultWidth) {
+ const studentColumnWidth = gradebookColumnSizeSetting
+ ? parseInt(gradebookColumnSizeSetting, 10)
+ : defaultWidth
return {
- id: 'student',
- type: 'student',
+ id: columnId,
+ type: columnId,
width: studentColumnWidth,
cssClass: 'meta-cell primary-column student',
headerCssClass: 'primary-column student',
@@ -2429,9 +2431,28 @@ class Gradebook extends React.Component {
initGrid() {
let assignmentGroup, assignmentGroupColumn, id
this.updateFilteredContentInfo()
- const studentColumn = this.buildStudentColumn()
+ const studentColumn = this.buildStudentColumn(
+ 'student',
+ this.gradebookColumnSizeSettings?.student,
+ 150
+ )
this.gridData.columns.definitions[studentColumn.id] = studentColumn
this.gridData.columns.frozen.push(studentColumn.id)
+ const studentColumnLastName = this.buildStudentColumn(
+ 'student_lastname',
+ this.gradebookColumnSizeSettings?.student_lastname,
+ 155
+ )
+ this.gridData.columns.definitions[studentColumnLastName.id] = studentColumnLastName
+ this.gridData.columns.frozen.push(studentColumnLastName.id)
+ const studentColumnFirstName = this.buildStudentColumn(
+ 'student_firstname',
+ this.gradebookColumnSizeSettings?.student_firstname,
+ 155
+ )
+ this.gridData.columns.definitions[studentColumnFirstName.id] = studentColumnFirstName
+ this.gridData.columns.frozen.push(studentColumnFirstName.id)
+
const ref2 = this.assignmentGroups
for (id in ref2) {
assignmentGroup = ref2[id]
@@ -2471,7 +2492,10 @@ class Gradebook extends React.Component {
})
this.gradebookGrid.gridSupport.initialize()
this.gradebookGrid.gridSupport.events.onActiveLocationChanged.subscribe((event, location) => {
- if (location.columnId === 'student' && location.region === 'body') {
+ if (
+ ['student', 'student_lastname'].includes(location.columnId) &&
+ location.region === 'body'
+ ) {
// In IE11, if we're navigating into the student column from a grade
// input cell with no text, this focus() call will select the
// instead of the grades link. Delaying the call (even with no actual
@@ -3054,6 +3078,13 @@ class Gradebook extends React.Component {
: undefined
}
+ updateStudentColumnHeaders() {
+ const columnIds = this.gridDisplaySettings.showSeparateFirstLastNames
+ ? ['student_lastname', 'student_firstname']
+ : ['student']
+ return this.updateColumnHeaders(columnIds)
+ }
+
handleHeaderKeyDown(e, columnId) {
return this.gradebookGrid.gridSupport.navigation.handleHeaderKeyDown(e, {
region: 'header',
@@ -3730,7 +3761,7 @@ class Gradebook extends React.Component {
this.saveSettings()
if (!skipRedraw) {
this.buildRows()
- return this.gradebookGrid.gridSupport.columns.updateColumnHeaders(['student'])
+ return this.updateStudentColumnHeaders()
}
}
@@ -3759,7 +3790,7 @@ class Gradebook extends React.Component {
this.saveSettings()
if (!skipRedraw) {
this.buildRows()
- return this.gradebookGrid.gridSupport.columns.updateColumnHeaders(['student'])
+ return this.updateStudentColumnHeaders()
}
}
@@ -3824,7 +3855,7 @@ class Gradebook extends React.Component {
}
updateStudentHeadersAndReloadData() {
- this.gradebookGrid.gridSupport.columns.updateColumnHeaders(['student'])
+ this.updateStudentColumnHeaders()
return this.dataLoader.reloadStudentDataForEnrollmentFilterChange()
}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/CellFormatterFactory.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/CellFormatterFactory.js
index a8db636464e..90f796b7e15 100644
--- a/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/CellFormatterFactory.js
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/CellFormatterFactory.js
@@ -20,6 +20,8 @@ import AssignmentCellFormatter from './AssignmentCellFormatter'
import AssignmentGroupCellFormatter from './AssignmentGroupCellFormatter'
import CustomColumnCellFormatter from './CustomColumnCellFormatter'
import StudentCellFormatter from './StudentCellFormatter'
+import StudentLastNameCellFormatter from './StudentLastNameCellFormatter'
+import StudentFirstNameCellFormatter from './StudentFirstNameCellFormatter'
import TotalGradeCellFormatter from './TotalGradeCellFormatter'
import TotalGradeOverrideCellFormatter from './TotalGradeOverrideCellFormatter'
@@ -30,6 +32,8 @@ class CellFormatterFactory {
assignment_group: new AssignmentGroupCellFormatter(),
custom_column: new CustomColumnCellFormatter(),
student: new StudentCellFormatter(gradebook),
+ student_lastname: new StudentLastNameCellFormatter(gradebook),
+ student_firstname: new StudentFirstNameCellFormatter(gradebook),
total_grade: new TotalGradeCellFormatter(gradebook),
total_grade_override: new TotalGradeOverrideCellFormatter(gradebook)
}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentCellFormatter.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentCellFormatter.js
index ddeffe15258..24df99b3ba2 100644
--- a/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentCellFormatter.js
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentCellFormatter.js
@@ -19,95 +19,15 @@
import $ from 'jquery'
import I18n from 'i18n!gradebook'
import '@canvas/jquery/jquery.instructure_misc_helpers' // $.toSentence
-import htmlEscape from 'html-escape'
-
-function getSecondaryDisplayInfo(student, secondaryInfo, options) {
- if (options.shouldShowSections() && secondaryInfo === 'section') {
- const sectionNames = student.sections
- .filter(options.isVisibleSection)
- .map(sectionId => options.getSection(sectionId).name)
- return $.toSentence(sectionNames.sort())
- }
-
- if (options.shouldShowGroups() && secondaryInfo === 'group') {
- const groupNames = student.group_ids.map(groupId => options.getGroup(groupId).name)
- return $.toSentence(groupNames.sort())
- }
-
- return {
- login_id: student.login_id,
- sis_id: student.sis_user_id,
- integration_id: student.integration_id
- }[secondaryInfo]
-}
-
-function getEnrollmentLabel(student) {
- if (student.isConcluded) {
- return I18n.t('concluded')
- }
- if (student.isInactive) {
- return I18n.t('inactive')
- }
-
- return null
-}
-
-// xsslint safeString.property enrollmentLabel secondaryInfo studentId courseId url displayName
-function render(options) {
- let enrollmentStatus = ''
- let secondaryInfo = ''
-
- if (options.enrollmentLabel) {
- const title = I18n.t('This user is currently not able to access the course')
- // xsslint safeString.identifier title
- enrollmentStatus = ` ${options.enrollmentLabel}`
- }
-
- if (options.secondaryInfo) {
- secondaryInfo = `${options.secondaryInfo}
`
- }
-
- // xsslint safeString.identifier enrollmentStatus secondaryInfo
- return `
-
- ${secondaryInfo}
- `
-}
+import {
+ getSecondaryDisplayInfo,
+ getEnrollmentLabel,
+ getOptions,
+ renderCell} from './StudentCellFormatter.utils'
export default class StudentCellFormatter {
constructor(gradebook) {
- this.options = {
- courseId: gradebook.options.context_id,
- getSection(sectionId) {
- return gradebook.sections[sectionId]
- },
- getGroup(groupId) {
- return gradebook.studentGroups[groupId]
- },
- getSelectedPrimaryInfo() {
- return gradebook.getSelectedPrimaryInfo()
- },
- getSelectedSecondaryInfo() {
- return gradebook.getSelectedSecondaryInfo()
- },
- isVisibleSection(sectionId) {
- return gradebook.sections[sectionId] != null
- },
- shouldShowSections() {
- return gradebook.showSections()
- },
- shouldShowGroups() {
- return gradebook.showStudentGroups()
- }
- }
+ this.options = getOptions(gradebook)
}
render = (_row, _cell, _value, _columnDef, student /* dataContext */) => {
@@ -127,6 +47,6 @@ export default class StudentCellFormatter {
url: `${student.enrollments[0].grades.html_url}#tab-assignments`
}
- return render(options)
+ return renderCell(options)
}
}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentCellFormatter.utils.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentCellFormatter.utils.js
new file mode 100644
index 00000000000..8aee862d4d7
--- /dev/null
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentCellFormatter.utils.js
@@ -0,0 +1,109 @@
+/*
+ * 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 .
+ */
+
+import $ from 'jquery'
+import htmlEscape from 'html-escape'
+import I18n from 'i18n!gradebook'
+
+export function getSecondaryDisplayInfo(student, secondaryInfo, options) {
+ if (options.shouldShowSections() && secondaryInfo === 'section') {
+ const sectionNames = student.sections
+ .filter(options.isVisibleSection)
+ .map(sectionId => options.getSection(sectionId).name)
+ return $.toSentence(sectionNames.sort())
+ }
+
+ if (options.shouldShowGroups() && secondaryInfo === 'group') {
+ const groupNames = student.group_ids.map(groupId => options.getGroup(groupId).name)
+ return $.toSentence(groupNames.sort())
+ }
+
+ return {
+ login_id: student.login_id,
+ sis_id: student.sis_user_id,
+ integration_id: student.integration_id
+ }[secondaryInfo]
+}
+
+export function getEnrollmentLabel(student) {
+ if (student.isConcluded) {
+ return I18n.t('concluded')
+ }
+ if (student.isInactive) {
+ return I18n.t('inactive')
+ }
+
+ return null
+}
+
+export function getOptions(gradebook) {
+ return {
+ courseId: gradebook.options.context_id,
+ getSection(sectionId) {
+ return gradebook.sections[sectionId]
+ },
+ getGroup(groupId) {
+ return gradebook.studentGroups[groupId]
+ },
+ getSelectedPrimaryInfo() {
+ return gradebook.getSelectedPrimaryInfo()
+ },
+ getSelectedSecondaryInfo() {
+ return gradebook.getSelectedSecondaryInfo()
+ },
+ isVisibleSection(sectionId) {
+ return gradebook.sections[sectionId] != null
+ },
+ shouldShowSections() {
+ return gradebook.showSections()
+ },
+ shouldShowGroups() {
+ return gradebook.showStudentGroups()
+ }
+ }
+}
+
+// xsslint safeString.property enrollmentLabel secondaryInfo studentId courseId url displayName
+export function renderCell(options) {
+ let enrollmentStatus = ''
+ let secondaryInfo = ''
+
+ if (options.enrollmentLabel) {
+ const title = I18n.t('This user is currently not able to access the course')
+ // xsslint safeString.identifier title
+ enrollmentStatus = ` ${options.enrollmentLabel}`
+ }
+
+ if (options.secondaryInfo) {
+ secondaryInfo = `${options.secondaryInfo}
`
+ }
+
+ // xsslint safeString.identifier enrollmentStatus secondaryInfo
+ return `
+
+ ${secondaryInfo}
+ `
+}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentFirstNameCellFormatter.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentFirstNameCellFormatter.js
new file mode 100644
index 00000000000..4761b06de33
--- /dev/null
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentFirstNameCellFormatter.js
@@ -0,0 +1,46 @@
+/*
+ * 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 .
+ */
+
+import I18n from 'i18n!gradebook'
+import '@canvas/jquery/jquery.instructure_misc_helpers' // $.toSentence
+import htmlEscape from 'html-escape'
+import {getEnrollmentLabel, renderCell} from './StudentCellFormatter.utils'
+
+export default class StudentFirstNameCellFormatter {
+ constructor(gradebook) {
+ this.options = {
+ courseId: gradebook.options.context_id
+ }
+ }
+
+ render = (_row, _cell, _value, _columnDef, student /* dataContext */) => {
+ if (student.isPlaceholder) {
+ return ''
+ }
+
+ const options = {
+ courseId: this.options.courseId,
+ displayName: student.first_name,
+ enrollmentLabel: getEnrollmentLabel(student),
+ studentId: student.id,
+ url: `${student.enrollments[0].grades.html_url}#tab-assignments`
+ }
+
+ return renderCell(options)
+ }
+}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentLastNameCellFormatter.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentLastNameCellFormatter.js
new file mode 100644
index 00000000000..c793b769426
--- /dev/null
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/formatters/StudentLastNameCellFormatter.js
@@ -0,0 +1,51 @@
+/*
+ * 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 .
+ */
+
+import $ from 'jquery'
+import I18n from 'i18n!gradebook'
+import '@canvas/jquery/jquery.instructure_misc_helpers' // $.toSentence
+import {
+ getSecondaryDisplayInfo,
+ getEnrollmentLabel,
+ getOptions,
+ renderCell} from './StudentCellFormatter.utils'
+
+export default class StudentLastNameCellFormatter {
+ constructor(gradebook) {
+ this.options = getOptions(gradebook)
+ }
+
+ render = (_row, _cell, _value, _columnDef, student /* dataContext */) => {
+ if (student.isPlaceholder) {
+ return ''
+ }
+
+ const secondaryInfo = this.options.getSelectedSecondaryInfo()
+
+ const options = {
+ courseId: this.options.courseId,
+ displayName: student.last_name,
+ enrollmentLabel: getEnrollmentLabel(student),
+ secondaryInfo: getSecondaryDisplayInfo(student, secondaryInfo, this.options),
+ studentId: student.id,
+ url: `${student.enrollments[0].grades.html_url}#tab-assignments`
+ }
+
+ return renderCell(options)
+ }
+}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/ColumnHeaderRenderer.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/ColumnHeaderRenderer.js
index 4fd2a43ac34..19233b09004 100644
--- a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/ColumnHeaderRenderer.js
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/ColumnHeaderRenderer.js
@@ -19,7 +19,10 @@
import AssignmentColumnHeaderRenderer from './AssignmentColumnHeaderRenderer'
import AssignmentGroupColumnHeaderRenderer from './AssignmentGroupColumnHeaderRenderer'
import CustomColumnHeaderRenderer from './CustomColumnHeaderRenderer'
+import StudentColumnHeader from './StudentColumnHeader'
import StudentColumnHeaderRenderer from './StudentColumnHeaderRenderer'
+import StudentLastNameColumnHeader from './StudentLastNameColumnHeader'
+import StudentFirstNameColumnHeaderRenderer from './StudentFirstNameColumnHeaderRenderer'
import TotalGradeColumnHeaderRenderer from './TotalGradeColumnHeaderRenderer'
import TotalGradeOverrideColumnHeaderRenderer from './TotalGradeOverrideColumnHeaderRenderer'
@@ -30,7 +33,13 @@ export default class ColumnHeaderRenderer {
assignment: new AssignmentColumnHeaderRenderer(gradebook),
assignment_group: new AssignmentGroupColumnHeaderRenderer(gradebook),
custom_column: new CustomColumnHeaderRenderer(gradebook),
- student: new StudentColumnHeaderRenderer(gradebook),
+ student: new StudentColumnHeaderRenderer(gradebook, StudentColumnHeader, 'student'),
+ student_lastname: new StudentColumnHeaderRenderer(
+ gradebook,
+ StudentLastNameColumnHeader,
+ 'student_lastname'
+ ),
+ student_firstname: new StudentFirstNameColumnHeaderRenderer(gradebook),
total_grade: new TotalGradeColumnHeaderRenderer(gradebook),
total_grade_override: new TotalGradeOverrideColumnHeaderRenderer(gradebook)
}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeader.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeader.js
index 8811ae79398..8471fc65b03 100644
--- a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeader.js
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeader.js
@@ -70,6 +70,18 @@ export default class StudentColumnHeader extends ColumnHeader {
...ColumnHeader.defaultProps
}
+ getColumnHeaderName() {
+ return I18n.t('Student Name')
+ }
+
+ getColumnHeaderOptions() {
+ return I18n.t('Student Name Options')
+ }
+
+ showDisplayAsViewOption() {
+ return true
+ }
+
onShowSectionNames = () => {
this.onSelectSecondaryInfo('section')
}
@@ -228,7 +240,7 @@ export default class StudentColumnHeader extends ColumnHeader {
padding="0 0 0 small"
>
- {I18n.t('Student Name')}
+ {this.getColumnHeaderName()}
@@ -246,7 +258,7 @@ export default class StudentColumnHeader extends ColumnHeader {
variant="icon"
icon={IconMoreSolid}
>
- {I18n.t('Student Name Options')}
+ {this.getColumnHeaderOptions()}
}
onToggle={this.onToggle}
@@ -254,30 +266,32 @@ export default class StudentColumnHeader extends ColumnHeader {
>
{sortMenu}
-
+
+ {studentRowHeaderConstants.primaryInfoLabels.first_last}
+
+
+ {studentRowHeaderConstants.primaryInfoLabels.last_first}
+
+
+
+ )}
{
- gradebook.handleHeaderKeyDown(event, columnId)
- },
- onMenuDismiss() {
- setTimeout(gradebook.handleColumnHeaderMenuClose)
- },
- onSelectPrimaryInfo: gradebook.setSelectedPrimaryInfo,
- onSelectSecondaryInfo: gradebook.setSelectedSecondaryInfo,
- onToggleEnrollmentFilter: gradebook.toggleEnrollmentFilter,
- removeGradebookElement: gradebook.keyboardNav.removeGradebookElement,
- sectionsEnabled: gradebook.sections_enabled,
- selectedEnrollmentFilters: gradebook.getSelectedEnrollmentFilters(),
- selectedPrimaryInfo: gradebook.getSelectedPrimaryInfo(),
- selectedSecondaryInfo: gradebook.getSelectedSecondaryInfo(),
- sisName: gradebook.options.sis_name,
- sortBySetting: {
- direction,
- disabled: !gradebook.contentLoadStates.studentsLoaded,
- isSortColumn: sortRowsBySetting.columnId === columnId,
- // sort functions with additional sort options enabled
- onSortBySortableName: () => {
- gradebook.setSortRowsBySetting(columnId, 'sortable_name', direction)
- },
- onSortBySisId: () => {
- gradebook.setSortRowsBySetting(columnId, 'sis_user_id', direction)
- },
- onSortByIntegrationId: () => {
- gradebook.setSortRowsBySetting(columnId, 'integration_id', direction)
- },
- onSortByLoginId: () => {
- gradebook.setSortRowsBySetting(columnId, 'login_id', direction)
- },
- onSortInAscendingOrder: () => {
- gradebook.setSortRowsBySetting(columnId, studentSettingKey, 'ascending')
- },
- onSortInDescendingOrder: () => {
- gradebook.setSortRowsBySetting(columnId, studentSettingKey, 'descending')
- },
- // sort functions with additional sort options disabled
- onSortBySortableNameAscending: () => {
- gradebook.setSortRowsBySetting(columnId, 'sortable_name', 'ascending')
- },
- onSortBySortableNameDescending: () => {
- gradebook.setSortRowsBySetting(columnId, 'sortable_name', 'descending')
- },
- settingKey
- },
- studentGroupsEnabled: gradebook.showStudentGroups()
- }
-}
+import {getProps} from './StudentColumnHeaderRenderer.utils'
export default class StudentColumnHeaderRenderer {
- constructor(gradebook) {
+ constructor(gradebook, element, columnName) {
this.gradebook = gradebook
+ this.element = element
+ this.columnName = columnName
}
render(_column, $container, _gridSupport, options) {
- const props = getProps(this.gradebook, options)
- ReactDOM.render(, $container)
+ const Element = this.element
+ const props = getProps(this.gradebook, options, this.columnName)
+ ReactDOM.render(, $container)
}
destroy(_column, $container, _gridSupport) {
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRenderer.utils.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRenderer.utils.js
new file mode 100644
index 00000000000..3035c5b8a70
--- /dev/null
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentColumnHeaderRenderer.utils.js
@@ -0,0 +1,80 @@
+/*
+ * 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 .
+ */
+
+export function getProps(gradebook, options, columnHeaderName) {
+ const columnId = columnHeaderName
+ const sortRowsBySetting = gradebook.getSortRowsBySetting()
+ const {columnId: currentColumnId, direction, settingKey} = sortRowsBySetting
+
+ const studentSettingKey = currentColumnId === columnHeaderName ? settingKey : 'sortable_name'
+
+ return {
+ ref: options.ref,
+ addGradebookElement: gradebook.keyboardNav.addGradebookElement,
+ disabled: !gradebook.contentLoadStates.studentsLoaded,
+ loginHandleName: gradebook.options.login_handle_name,
+ onHeaderKeyDown: event => {
+ gradebook.handleHeaderKeyDown(event, columnId)
+ },
+ onMenuDismiss() {
+ setTimeout(gradebook.handleColumnHeaderMenuClose)
+ },
+ onSelectPrimaryInfo: gradebook.setSelectedPrimaryInfo,
+ onSelectSecondaryInfo: gradebook.setSelectedSecondaryInfo,
+ onToggleEnrollmentFilter: gradebook.toggleEnrollmentFilter,
+ removeGradebookElement: gradebook.keyboardNav.removeGradebookElement,
+ sectionsEnabled: gradebook.sections_enabled,
+ selectedEnrollmentFilters: gradebook.getSelectedEnrollmentFilters(),
+ selectedPrimaryInfo: gradebook.getSelectedPrimaryInfo(),
+ selectedSecondaryInfo: gradebook.getSelectedSecondaryInfo(),
+ sisName: gradebook.options.sis_name,
+ sortBySetting: {
+ direction,
+ disabled: !gradebook.contentLoadStates.studentsLoaded,
+ isSortColumn: sortRowsBySetting.columnId === columnId,
+ // sort functions with additional sort options enabled
+ onSortBySortableName: () => {
+ gradebook.setSortRowsBySetting(columnId, 'sortable_name', direction)
+ },
+ onSortBySisId: () => {
+ gradebook.setSortRowsBySetting(columnId, 'sis_user_id', direction)
+ },
+ onSortByIntegrationId: () => {
+ gradebook.setSortRowsBySetting(columnId, 'integration_id', direction)
+ },
+ onSortByLoginId: () => {
+ gradebook.setSortRowsBySetting(columnId, 'login_id', direction)
+ },
+ onSortInAscendingOrder: () => {
+ gradebook.setSortRowsBySetting(columnId, studentSettingKey, 'ascending')
+ },
+ onSortInDescendingOrder: () => {
+ gradebook.setSortRowsBySetting(columnId, studentSettingKey, 'descending')
+ },
+ // sort functions with additional sort options disabled
+ onSortBySortableNameAscending: () => {
+ gradebook.setSortRowsBySetting(columnId, 'sortable_name', 'ascending')
+ },
+ onSortBySortableNameDescending: () => {
+ gradebook.setSortRowsBySetting(columnId, 'sortable_name', 'descending')
+ },
+ settingKey
+ },
+ studentGroupsEnabled: gradebook.showStudentGroups()
+ }
+}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeader.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeader.js
new file mode 100644
index 00000000000..3a7bce015ac
--- /dev/null
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeader.js
@@ -0,0 +1,62 @@
+/*
+ * 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 .
+ */
+
+import React from 'react'
+import {Grid} from '@instructure/ui-grid'
+import {View} from '@instructure/ui-view'
+
+import {Text} from '@instructure/ui-text'
+import I18n from 'i18n!gradebook'
+import ColumnHeader from './ColumnHeader'
+
+export default class StudentFirstNameColumnHeader extends ColumnHeader {
+ static propTypes = {
+ ...ColumnHeader.propTypes
+ }
+
+ static defaultProps = {
+ ...ColumnHeader.defaultProps
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+ {I18n.t('Student First Name')}
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeaderRenderer.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeaderRenderer.js
new file mode 100644
index 00000000000..6f134209dda
--- /dev/null
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentFirstNameColumnHeaderRenderer.js
@@ -0,0 +1,50 @@
+/*
+ * 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 .
+ */
+
+import React from 'react'
+import ReactDOM from 'react-dom'
+import StudentFirstNameColumnHeader from './StudentFirstNameColumnHeader'
+
+function getProps(gradebook, options) {
+ const columnId = 'student_firstname'
+
+ return {
+ ref: options.ref,
+ addGradebookElement: gradebook.keyboardNav.addGradebookElement,
+ disabled: !gradebook.contentLoadStates.studentsLoaded,
+ removeGradebookElement: gradebook.keyboardNav.removeGradebookElement,
+ onHeaderKeyDown: event => {
+ gradebook.handleHeaderKeyDown(event, columnId)
+ }
+ }
+}
+
+export default class StudentFirstNameColumnHeaderRenderer {
+ constructor(gradebook) {
+ this.gradebook = gradebook
+ }
+
+ render(_column, $container, _gridSupport, options) {
+ const props = getProps(this.gradebook, options)
+ ReactDOM.render(, $container)
+ }
+
+ destroy(_column, $container, _gridSupport) {
+ ReactDOM.unmountComponentAtNode($container)
+ }
+}
diff --git a/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentLastNameColumnHeader.js b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentLastNameColumnHeader.js
new file mode 100644
index 00000000000..3afb5f588ed
--- /dev/null
+++ b/ui/features/gradebook/react/default_gradebook/GradebookGrid/headers/StudentLastNameColumnHeader.js
@@ -0,0 +1,34 @@
+/*
+ * 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 .
+ */
+
+import I18n from 'i18n!gradebook'
+import StudentColumnHeader from './StudentColumnHeader'
+
+export default class StudentLastNameColumnHeader extends StudentColumnHeader {
+ getColumnHeaderName() {
+ return I18n.t('Student Last Name')
+ }
+
+ getColumnHeaderOptions() {
+ return I18n.t('Student Last Name Options')
+ }
+
+ showDisplayAsViewOption() {
+ return false
+ }
+}