enhance individual gradebook graphql setup

setup enhanced individual gradebook to use graphql to fetch data for
the Content Selection components and display in the dropdowns. Add
functionality to get correct student/assignment via query params and
update the query params when student/assignment changes. Also wire up
some of the other components needed for enhanced individual gradebook
with placeholder text

closes EVAL-3115
flag=individual_gradebook_enhancements

test plan:
- this change is still a work in progress, but you should be able to
  see the dropdowns in the enhanced individual gradebook and select a
  student and assignment. The student and assignment should be reflected
  in the url query params. The dropdowns should be populated with the
  correct data from the graphql queries

Change-Id: Iefeaaae1c314bd098f0ba9197c8523605cfe78b3
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/318311
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Aaron Shafovaloff <ashafovaloff@instructure.com>
QA-Review: Aaron Shafovaloff <ashafovaloff@instructure.com>
Product-Review: Aaron Shafovaloff <ashafovaloff@instructure.com>
This commit is contained in:
Chris Soto 2023-05-16 14:17:27 -06:00 committed by Aaron Shafovaloff
parent 978d6ec5f1
commit 1f5901032d
11 changed files with 870 additions and 0 deletions

View File

@ -23,3 +23,4 @@
<div>
<span data-component="GradebookSelector"></span>
</div>
<div data-component="EnhancedIndividualGradebook"></div>

View File

@ -19,6 +19,8 @@
import React from 'react'
import ReactDOM from 'react-dom'
import GradebookMenu from '@canvas/gradebook-menu'
import {BrowserRouter, Routes, Route} from 'react-router-dom'
import EnhancedIndividualGradebookWrapper from './react/EnhancedIndividualGradebookWrapper'
ReactDOM.render(
<GradebookMenu
@ -31,3 +33,15 @@ ReactDOM.render(
/>,
document.querySelector('[data-component="GradebookSelector"]')
)
const matches = window.location.pathname.match(/(.*\/gradebook)/)
const baseUrl = (matches && matches[0]) || ''
ReactDOM.render(
<BrowserRouter basename={baseUrl}>
<Routes>
<Route path="/" element={<EnhancedIndividualGradebookWrapper />} />
</Routes>
</BrowserRouter>,
document.querySelector('[data-component="EnhancedIndividualGradebook"]')
)

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 - 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 gql from 'graphql-tag'
export const GRADEBOOK_QUERY = gql`
query GradebookQuery($courseId: ID!) {
course(id: $courseId) {
enrollmentsConnection(filter: {types: StudentEnrollment}) {
nodes {
user {
id: _id
name
sortableName
}
}
}
submissionsConnection {
nodes {
grade
id: _id
score
assignment {
id: _id
}
}
}
assignmentsConnection {
nodes {
id: _id
name
pointsPossible
}
}
}
}
`

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 - 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 {View} from '@instructure/ui-view'
import {useScope as useI18nScope} from '@canvas/i18n'
import {AssignmentConnectionResponse, SubmissionConnectionResponse} from '../types'
const I18n = useI18nScope('enhanced_individual_gradebook')
type Props = {
assignment?: AssignmentConnectionResponse
submissions?: SubmissionConnectionResponse[]
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function AssignmentInformation({assignment, submissions}: Props) {
return (
<View as="div">
<View as="div" className="row-fluid">
<View as="div" className="span4">
<View as="h2">{I18n.t('Assignment Information')}</View>
</View>
<View as="div" className="span8 pad-box top-only">
<View as="p" className="submission_selection">
{I18n.t('Select an assignment to view additional information here.')}
</View>
</View>
</View>
</View>
)
}

View File

@ -0,0 +1,217 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {useEffect, useState} from 'react'
import {useScope as useI18nScope} from '@canvas/i18n'
import {AssignmentConnectionResponse, UserConnectionResponse} from '../types'
import {View} from '@instructure/ui-view'
const I18n = useI18nScope('enhanced_individual_gradebook')
type Props = {
assignments: AssignmentConnectionResponse[]
students: UserConnectionResponse[]
selectedStudentId?: string | null
selectedAssignmentId?: string | null
onStudentChange: (student?: UserConnectionResponse) => void
onAssignmentChange: (assignment?: AssignmentConnectionResponse) => void
}
// TODO: might want a map for quicker lookups
type DropDownOption<T> = {
id: string
name: string
data?: T
}
type StudentDropdownOption = DropDownOption<UserConnectionResponse>[]
type AssignmentDropdownOption = DropDownOption<AssignmentConnectionResponse>[]
const DEFAULT_STUDENT_DROPDOWN_TEXT = I18n.t('No Student Selected')
const DEFAULT_ASSIGNMENT_DROPDOWN_TEXT = I18n.t('No Assignment Selected')
const defaultStudentDropdownOptions = {id: '-1', name: DEFAULT_STUDENT_DROPDOWN_TEXT}
const defaultAssignmentDropdownOptions = {id: '-1', name: DEFAULT_ASSIGNMENT_DROPDOWN_TEXT}
export default function ContentSelection({
students,
assignments,
selectedAssignmentId,
selectedStudentId,
onAssignmentChange,
onStudentChange,
}: Props) {
const [studentDropdownOptions, setStudentDropdownOptions] = useState<StudentDropdownOption>([])
const [assignmentDropdownOptions, setAssignmentDropdownOptions] =
useState<AssignmentDropdownOption>([])
const [selectedStudentIndex, setSelectedStudentIndex] = useState<number>(0)
const [selectedAssignmentIndex, setSelectedAssignmentIndex] = useState<number>(0)
// TOOD: might be ablet to refactor to make simpler
useEffect(() => {
const studentOptions: StudentDropdownOption = [
defaultStudentDropdownOptions,
...students.map(student => ({id: student.id, name: student.sortableName, data: student})),
]
setStudentDropdownOptions(studentOptions)
if (selectedStudentId) {
const studentIndex = studentOptions.findIndex(
studentOption => studentOption.id === selectedStudentId
)
if (studentIndex !== -1) {
setSelectedStudentIndex(studentIndex)
}
}
const assignmentOptions: AssignmentDropdownOption = [
defaultAssignmentDropdownOptions,
...assignments.map(assignment => ({
id: assignment.id,
name: assignment.name,
data: assignment,
})),
]
setAssignmentDropdownOptions(assignmentOptions)
if (selectedAssignmentId) {
const assignmentIndex = assignmentOptions.findIndex(
assignmentOption => assignmentOption.id === selectedAssignmentId
)
if (assignmentIndex >= 0) {
setSelectedAssignmentIndex(assignmentIndex)
}
}
}, [students, assignments, selectedStudentId, selectedAssignmentId])
const handleChangeStudent = (event?: React.ChangeEvent<HTMLSelectElement>, newIndex?: number) => {
const selectedIndex = (event ? event.target.selectedIndex : newIndex) ?? 0
setSelectedStudentIndex(selectedIndex)
const selectedStudent = studentDropdownOptions[selectedIndex]?.data
onStudentChange(selectedStudent)
}
const handleChangeAssignment = (
event?: React.ChangeEvent<HTMLSelectElement>,
newIndex?: number
) => {
const selectedIndex = (event ? event.target.selectedIndex : newIndex) ?? 0
setSelectedAssignmentIndex(selectedIndex)
const selectedAssignment = assignmentDropdownOptions[selectedIndex]?.data
onAssignmentChange(selectedAssignment)
}
return (
<>
<View as="div" className="row-fluid">
<View as="div" className="span12">
<View as="h2">{I18n.t('Content Selection')}</View>
</View>
</View>
<View as="div" className="row-fluid pad-box bottom-only">
<View as="div" className="span4 text-right-responsive">
<label htmlFor="student_select" style={{textAlign: 'right', display: 'block'}}>
{I18n.t('Select a student')}
</label>
</View>
<View as="div" className="span8">
<select
className="student_select"
onChange={handleChangeStudent}
value={studentDropdownOptions[selectedStudentIndex]?.id}
>
{studentDropdownOptions.map(option => (
<option key={option.id} value={option.id}>
{option.name}
</option>
))}
</select>
<View as="div" className="row-fluid pad-box bottom-only student_navigation">
<View as="div" className="span4">
<button
type="button"
className="btn btn-block next_object"
disabled={selectedStudentIndex <= 1}
onClick={() => handleChangeStudent(undefined, selectedStudentIndex - 1)}
>
{I18n.t('Previous Student')}
</button>
</View>
<View as="div" className="span4">
<button
type="button"
className="btn btn-block next_object"
disabled={selectedStudentIndex >= studentDropdownOptions.length - 1}
onClick={() => handleChangeStudent(undefined, selectedStudentIndex + 1)}
>
{I18n.t('Next Student')}
</button>
</View>
</View>
</View>
</View>
<View as="div" className="row-fluid pad-box bottom-only">
<View as="div" className="span4 text-right-responsive">
<label htmlFor="assignment_select" style={{textAlign: 'right', display: 'block'}}>
{I18n.t('Select an assignment')}
</label>
</View>
<View as="div" className="span8">
<select
className="assignment_select"
onChange={handleChangeAssignment}
value={assignmentDropdownOptions[selectedAssignmentIndex]?.id}
>
{assignmentDropdownOptions.map(option => (
<option key={option.id} value={option.id}>
{option.name}
</option>
))}
</select>
<View as="div" className="row-fluid pad-box bottom-only assignment_navigation">
<View as="div" className="span4">
<button
type="button"
className="btn btn-block next_object"
disabled={selectedAssignmentIndex <= 1}
onClick={() => handleChangeAssignment(undefined, selectedAssignmentIndex - 1)}
>
{I18n.t('Previous Assignment')}
</button>
</View>
<View as="div" className="span4">
<button
type="button"
className="btn btn-block next_object"
disabled={selectedAssignmentIndex >= assignmentDropdownOptions.length - 1}
onClick={() => handleChangeAssignment(undefined, selectedAssignmentIndex + 1)}
>
{I18n.t('Next Assignment')}
</button>
</View>
</View>
</View>
</View>
</>
)
}

View File

@ -0,0 +1,140 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {useEffect, useState} from 'react'
import {useQuery} from 'react-apollo'
import {useSearchParams} from 'react-router-dom'
import {useScope as useI18nScope} from '@canvas/i18n'
import {View} from '@instructure/ui-view'
import AssignmentInformation from './AssignmentInformation'
import ContentSelection from './ContentSelection'
import GlobalSettings from './GlobalSettings'
import GradingResults from './GradingResults'
import StudentInformation from './StudentInformation'
import {
AssignmentConnectionResponse,
GradebookQueryResponse,
SubmissionConnectionResponse,
UserConnectionResponse,
} from '../types'
import {GRADEBOOK_QUERY} from '../queries/Queries'
const I18n = useI18nScope('enhanced_individual_gradebook')
const STUDENT_SEARCH_PARAM = 'student'
const ASSIGNMENT_SEARCH_PARAM = 'assignment'
export default function EnhancedIndividualGradebook() {
const [submissions, setSubmissions] = useState<SubmissionConnectionResponse[]>([])
const [students, setStudents] = useState<UserConnectionResponse[]>([])
const [assignments, setAssignments] = useState<AssignmentConnectionResponse[]>([])
const [selectedStudent, setSelectedStudent] = useState<UserConnectionResponse>()
const [selectedAssignment, setSelectedAssignment] = useState<AssignmentConnectionResponse>()
const [selectedSubmissions, setSelectedSubmissions] = useState<SubmissionConnectionResponse[]>([])
const courseId = ENV.GRADEBOOK_OPTIONS?.context_id // TODO: get from somewhere else?
const [searchParams, setSearhParams] = useSearchParams()
const selectedStudentId = searchParams.get(STUDENT_SEARCH_PARAM)
const selectedAssignmentId = searchParams.get(ASSIGNMENT_SEARCH_PARAM)
const {data, error} = useQuery<GradebookQueryResponse>(GRADEBOOK_QUERY, {
variables: {courseId},
fetchPolicy: 'cache-and-network',
skip: !courseId,
})
useEffect(() => {
if (error) {
// TODO: handle error
}
if (data?.course) {
const {assignmentsConnection, enrollmentsConnection, submissionsConnection} = data.course
setAssignments(assignmentsConnection.nodes)
setSubmissions(submissionsConnection.nodes)
const studentEnrollments = enrollmentsConnection.nodes.map(enrollment => enrollment.user)
const sortableStudents = studentEnrollments.sort((a, b) => {
return a.sortableName.localeCompare(b.sortableName)
})
setStudents(sortableStudents)
}
}, [data, error])
const handleStudentChange = (student?: UserConnectionResponse) => {
setSelectedStudent(student)
if (student) {
searchParams.set(STUDENT_SEARCH_PARAM, student?.id)
setSearhParams(searchParams)
} else {
searchParams.delete(STUDENT_SEARCH_PARAM)
}
}
const handleAssignmentChange = (assignment?: AssignmentConnectionResponse) => {
setSelectedAssignment(assignment)
setSelectedSubmissions(submissions.filter(s => s.assignment.id === assignment?.id))
if (assignment) {
searchParams.set(ASSIGNMENT_SEARCH_PARAM, assignment?.id)
setSearhParams(searchParams)
} else {
searchParams.delete(ASSIGNMENT_SEARCH_PARAM)
}
}
return (
<View as="div">
<View as="div" className="row-fluid">
<View as="div" className="span12">
<View as="h1">{I18n.t('Gradebook: Enhanced Individual View')}</View>
{I18n.t(
'Note: Grades and notes will be saved automatically after moving out of the field.'
)}
</View>
</View>
<GlobalSettings />
<div className="hr" style={{margin: 10, padding: 10, borderBottom: '1px solid #eee'}} />
<ContentSelection
assignments={assignments}
students={students}
selectedStudentId={selectedStudentId}
selectedAssignmentId={selectedAssignmentId}
onStudentChange={handleStudentChange}
onAssignmentChange={handleAssignmentChange}
/>
<div className="hr" style={{margin: 10, padding: 10, borderBottom: '1px solid #eee'}} />
<GradingResults />
<div className="hr" style={{margin: 10, padding: 10, borderBottom: '1px solid #eee'}} />
<StudentInformation student={selectedStudent} />
<div className="hr" style={{margin: 10, padding: 10, borderBottom: '1px solid #eee'}} />
<AssignmentInformation assignment={selectedAssignment} submissions={selectedSubmissions} />
</View>
)
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {useEffect, useState} from 'react'
import {ApolloProvider, createClient} from '@canvas/apollo'
import LoadingIndicator from '@canvas/loading-indicator'
import EnhancedIndividualGradebook from './EnhancedIndividualGradebook'
export default function EnhancedIndividualGradebookWrapper() {
const [client, setClient] = useState<any>(null) // TODO: remove <any>
const [loading, setLoading] = useState(true)
useEffect(() => {
const setupApolloClient = async () => {
// TODO: Properly set up cache
// const cache = await createPersistentCache([cache_key])
// setClient(createClient({cache}))
setClient(createClient())
setLoading(false)
}
setupApolloClient()
}, [])
if (loading) {
return <LoadingIndicator />
}
return (
<ApolloProvider client={client}>
<EnhancedIndividualGradebook />
</ApolloProvider>
)
}

View File

@ -0,0 +1,183 @@
/*
* Copyright (C) 2023 - 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 {useScope as useI18nScope} from '@canvas/i18n'
import {View} from '@instructure/ui-view'
const I18n = useI18nScope('enhanced_individual_gradebook')
export default function GlobalSettings() {
return (
<>
<View as="div" className="row-fluid">
<View as="div" className="span12">
<h2>{I18n.t('Global Settings')}</h2>
</View>
</View>
<View as="div" className="row-fluid">
<View as="div" className="span4">
<label htmlFor="section_select" style={{textAlign: 'right', display: 'block'}}>
{I18n.t('Select a section')}
</label>
</View>
<View as="div" className="span8">
{/* TODO: Get Sections */}
<select id="section_select" className="section_select">
<option value="all">{I18n.t('All Sections')}</option>
<option value="1">Section 1</option>
</select>
</View>
</View>
<div className="row-fluid" style={{paddingBottom: 20}}>
<View as="div" className="span4">
<label htmlFor="sort_select" style={{textAlign: 'right', display: 'block'}}>
{I18n.t('Sort Assignments')}
</label>
</View>
<View as="div" className="span8">
<select id="sort_select" className="section_select" defaultValue="alpha">
<option value="assignment_group">
{I18n.t('assignment_order_assignment_groups', 'By Assignment Group and Position')}
</option>
<option value="alpha">{I18n.t('assignment_order_alpha', 'Alphabetically')}</option>
<option value="due_date">{I18n.t('assignment_order_due_date', 'By Due Date')}</option>
</select>
</View>
</div>
<View as="div" className="row-fluid pad-box bottom-only">
<View as="div" className="span4">
{/* {{!-- Intentionally left empty so this scales to smaller screens --}} */}
</View>
<View as="div" className="span7">
<div
className="checkbox"
style={{padding: 12, margin: '10px 0px', background: '#eee', borderRadius: 5}}
>
<label className="checkbox" htmlFor="ungraded_checkbox">
<input type="checkbox" id="ungraded_checkbox" name="ungraded_checkbox" />
{I18n.t('View Ungraded as 0')}
</label>
</div>
<div
className="checkbox"
style={{padding: 12, margin: '10px 0px', background: '#eee', borderRadius: 5}}
>
<label className="checkbox" htmlFor="hide_names_checkbox">
<input type="checkbox" id="hide_names_checkbox" name="hide_names_checkbox" />
{I18n.t('Hide Student Names')}
</label>
</div>
<div
className="checkbox"
style={{padding: 12, margin: '10px 0px', background: '#eee', borderRadius: 5}}
>
<label className="checkbox" htmlFor="concluded_enrollments_checkbox">
<input
type="checkbox"
id="concluded_enrollments_checkbox"
name="concluded_enrollments_checkbox"
/>
{I18n.t('Show Concluded Enrollments')}
</label>
</div>
<div
className="checkbox"
style={{padding: 12, margin: '10px 0px', background: '#eee', borderRadius: 5}}
>
<label className="checkbox" htmlFor="show_notes_checkbox">
<input type="checkbox" id="show_notes_checkbox" name="show_notes_checkbox" />
{I18n.t('Show Notes in Student Info')}
</label>
</div>
{/* {{#if finalGradeOverrideEnabled}}
<View as="div" className="checkbox">
<label className="checkbox">
{{
input
type="checkbox"
id="allow_final_grade_override"
name="allow_final_grade_override"
checked=allowFinalGradeOverride
}}
{{#t}}Allow Final Grade Override{{/t}}
</label>
</View>
{{/if}} */}
{/* {{#unless gradesAreWeighted}}
<View as="div" className="checkbox">
<label className="checkbox">
{{
input
type="checkbox"
id="show_total_as_points"
name="show_total_as_points"
checked=showTotalAsPoints
}}
{{#t "show_total_as_points"}}Show Totals as Points on Student Grade Page{{/t}}
</label>
</View>
{{/unless}} */}
</View>
</View>
<View as="div" className="row-fluid">
<View as="div" className="span4">
{/* {{!-- Intentionally left empty so this scales to smaller screens --}} */}
</View>
<View as="div" className="span8">
<View as="div" className="pad-box bottom-only">
<button type="button" className="btn" id="gradebook-export">
<i className="icon-download" />
{I18n.t('Download Current Scores (.csv)')}
</button>
{/* {{#if lastGeneratedCsvAttachmentUrl}}
<a aria-label="{{unbound lastGeneratedCsvLabel}}" href="{{unbound lastGeneratedCsvAttachmentUrl}}" id="last-exported-gradebook">
{{unbound lastGeneratedCsvLabel}}
</a>
{{/if}} */}
</View>
<View as="div" className="pad-box bottom-only">
<a id="upload" className="btn" href="{{unbound uploadCsvUrl}}">
<i className="icon-upload" />
{I18n.t('Upload Scores (.csv)')}
</a>
</View>
{/* <iframe style="display:none" id="gradebook-export-iframe"></iframe> */}
<View as="div" className="pad-box bottom-only">
<View as="div">
{/* {{#if publishToSisEnabled}}
<a href="{{ unbound publishToSisURL }}">
{{#t}}Sync grades to SIS{{/t}}
</a>
{{/if}} */}
</View>
<View as="div">
<a href="{{ unbound gradingHistoryUrl }}">{I18n.t('View Gradebook History')}</a>
</View>
</View>
</View>
</View>
</>
)
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 - 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 {View} from '@instructure/ui-view'
import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('enhanced_individual_gradebook')
export default function GradingResults() {
return (
<>
<View as="div">
<View as="div" className="row-fluid">
<View as="div" className="span4">
<View as="h2">{I18n.t('Grading')}</View>
</View>
<View as="div" className="span8 pad-box top-only">
<View as="p" className="submission_selection">
{I18n.t('Select a student and an assignment to view and edit grades.')}
</View>
</View>
</View>
</View>
</>
)
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 - 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 {View} from '@instructure/ui-view'
import {useScope as useI18nScope} from '@canvas/i18n'
import {UserConnectionResponse} from '../types'
const I18n = useI18nScope('enhanced_individual_gradebook')
type Props = {
student?: UserConnectionResponse
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function StudentInformation({student}: Props) {
return (
<View as="div">
<View as="div" className="row-fluid">
<View as="div" className="span4">
<View as="h2">{I18n.t('Student Information')}</View>
</View>
<View as="div" className="span8 pad-box top-only">
<View as="p" className="submission_selection">
{I18n.t('Select a student to view additional information here.')}
</View>
</View>
</View>
</View>
)
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2023 - 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/>.
*/
/**
* Temporarily generate types for GraphQL queries until we have automated generation setup
*/
export type UserConnectionResponse = {
enrollments: {
section: {
name: string
id: string
}
}
email: string
id: string
loginId: string
name: string
sortableName: string
}
export type AssignmentConnectionResponse = {
id: string
name: string
}
export type SubmissionConnectionResponse = {
assignment: {
id: string
}
user: UserConnectionResponse
id: string
score: number
grade: string
}
export type GradebookQueryResponse = {
course: {
enrollmentsConnection: {
nodes: {
user: UserConnectionResponse
}[]
}
submissionsConnection: {
nodes: SubmissionConnectionResponse[]
}
assignmentsConnection: {
nodes: AssignmentConnectionResponse[]
}
}
}
export type UserSubmissionMap = {
[userId: string]: {
email: string
submissions: {
[assignmentId: string]: {
score: number
grade: string
}
}
}
}