Show first and last names columns in gradebook

Last name column has all the same fields as the original student
column. The first name column only shows the first name.

closes EVAL-1990
flag=gradebook_show_first_last_names

Test plan:
With the FF on and the account admin setting to show separate columns
enabled, as a teacher go to gradebook:

1. Select the "Split Student Names" view option
- See that the gradebook grid shows separate columns for student last and
first names
- Make sure all the column options in the last name column work "Sort by",
"Secondary Info" and "Show - Inactive/Concluded enrollments" and the
option to "Display as" is not there
- See that there are no column options for first name
- See that the assignment and student search work as before
2. Unselect the "Split Student Names" view option
- See that the grid shows the Student column as before

Change-Id: Ic3ffefcd2069eca84823d60bb993d98521c8861d
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/276013
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
Reviewed-by: Adrian Packel <apackel@instructure.com>
QA-Review: Dustin Cowles <dustin.cowles@instructure.com>
Product-Review: Syed Hussain <shussain@instructure.com>
This commit is contained in:
Syed Hussain 2021-10-13 12:54:37 -05:00
parent ae7a2ad851
commit 873f88e270
15 changed files with 738 additions and 198 deletions

View File

@ -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(() => {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 */

View File

@ -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 <body>
// 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()
}

View File

@ -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)
}

View File

@ -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 = `&nbsp;<span title="${title}" class="label">${options.enrollmentLabel}</span>`
}
if (options.secondaryInfo) {
secondaryInfo = `<div class="secondary-info">${options.secondaryInfo}</div>`
}
// xsslint safeString.identifier enrollmentStatus secondaryInfo
return `
<div class="student-name">
<a
class="student-grades-link student_context_card_trigger"
data-student_id="${options.studentId}"
data-course_id="${options.courseId}"
href="${options.url}"
>${htmlEscape(options.displayName)}</a>
${enrollmentStatus}
</div>
${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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 = `&nbsp;<span title="${title}" class="label">${options.enrollmentLabel}</span>`
}
if (options.secondaryInfo) {
secondaryInfo = `<div class="secondary-info">${options.secondaryInfo}</div>`
}
// xsslint safeString.identifier enrollmentStatus secondaryInfo
return `
<div class="student-name">
<a
class="student-grades-link student_context_card_trigger"
data-student_id="${options.studentId}"
data-course_id="${options.courseId}"
href="${options.url}"
>${htmlEscape(options.displayName)}</a>
${enrollmentStatus}
</div>
${secondaryInfo}
`
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View File

@ -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)
}

View File

@ -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"
>
<Text fontStyle="normal" size="x-small" weight="bold">
{I18n.t('Student Name')}
{this.getColumnHeaderName()}
</Text>
</View>
</Grid.Col>
@ -246,7 +258,7 @@ export default class StudentColumnHeader extends ColumnHeader {
variant="icon"
icon={IconMoreSolid}
>
<ScreenReaderContent>{I18n.t('Student Name Options')}</ScreenReaderContent>
<ScreenReaderContent>{this.getColumnHeaderOptions()}</ScreenReaderContent>
</Button>
}
onToggle={this.onToggle}
@ -254,6 +266,7 @@ export default class StudentColumnHeader extends ColumnHeader {
>
{sortMenu}
{this.showDisplayAsViewOption() && (
<Menu
label={I18n.t('Display as')}
contentRef={this.bindDisplayAsMenuContent}
@ -278,6 +291,7 @@ export default class StudentColumnHeader extends ColumnHeader {
</Menu.Item>
</Menu.Group>
</Menu>
)}
<Menu
contentRef={this.bindSecondaryInfoMenuContent}

View File

@ -18,79 +18,19 @@
import React from 'react'
import ReactDOM from 'react-dom'
import StudentColumnHeader from './StudentColumnHeader'
function getProps(gradebook, options) {
const columnId = 'student'
const sortRowsBySetting = gradebook.getSortRowsBySetting()
const {columnId: currentColumnId, direction, settingKey} = sortRowsBySetting
const studentSettingKey = currentColumnId === 'student' ? 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()
}
}
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(<StudentColumnHeader {...props} />, $container)
const Element = this.element
const props = getProps(this.gradebook, options, this.columnName)
ReactDOM.render(<Element {...props} />, $container)
}
destroy(_column, $container, _gridSupport) {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<div
className={`Gradebook__ColumnHeaderContent ${this.state.hasFocus ? 'focused' : ''}`}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
>
<div style={{flex: 1, minWidth: '1px'}}>
<Grid colSpacing="none" hAlign="space-between" vAlign="middle">
<Grid.Row>
<Grid.Col textAlign="start">
<View
className="Gradebook__ColumnHeaderDetail Gradebook__ColumnHeaderDetail--OneLine"
padding="0 0 0 small"
>
<Text fontStyle="normal" size="x-small" weight="bold">
{I18n.t('Student First Name')}
</Text>
</View>
</Grid.Col>
</Grid.Row>
</Grid>
</div>
</div>
)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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(<StudentFirstNameColumnHeader {...props} />, $container)
}
destroy(_column, $container, _gridSupport) {
ReactDOM.unmountComponentAtNode($container)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}