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 ` -
- ${htmlEscape(options.displayName)} - ${enrollmentStatus} -
- ${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 ` +
+ ${htmlEscape(options.displayName)} + ${enrollmentStatus} +
+ ${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} - - {I18n.t('Display as')}} + {this.showDisplayAsViewOption() && ( + - {I18n.t('Display as')}} > - {studentRowHeaderConstants.primaryInfoLabels.first_last} - - - {studentRowHeaderConstants.primaryInfoLabels.last_first} - - - + + {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 + } +}