Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

107 bulk enroll um user #177

Merged
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8696892
Creatated AddUMUsers page w/ stepper
chrisrrowland Jul 9, 2021
7372417
Move step content to functions
chrisrrowland Jul 9, 2021
44f3d51
Wire up get course sections w/ create sections FE
chrisrrowland Jul 20, 2021
ad43a0e
Merge upstream branch into main
chrisrrowland Aug 9, 2021
625a950
Merge branch 'main' into 107_bulk_enroll_um_user
chrisrrowland Aug 9, 2021
f9e14e4
Added functionless CreateSectionWidget
chrisrrowland Aug 10, 2021
9bb375f
Start of a section selector
chrisrrowland Aug 10, 2021
4f15988
Small layout correction
chrisrrowland Aug 11, 2021
b509718
Loading spinner and selection callback function for section list
chrisrrowland Aug 11, 2021
43e6778
Sort section list alphabetically
chrisrrowland Aug 12, 2021
dc09591
Work in progress. Create section widget
chrisrrowland Aug 16, 2021
7a362ae
Enter keys to create section
chrisrrowland Aug 17, 2021
f40682f
Fix a typescript warning.
chrisrrowland Aug 17, 2021
a9697fc
making tot_student field mandatory (#4)
pushyamig Aug 17, 2021
22baa16
Starting work on upload section
chrisrrowland Aug 17, 2021
2c55827
Merge branch '107_bulk_enroll_um_user' of https://github.com/chrisrro…
chrisrrowland Aug 17, 2021
7681141
need a minHeight so backdrop has somewhere to go
chrisrrowland Aug 17, 2021
83370d9
Initial work with confirmation
chrisrrowland Aug 18, 2021
9b176a0
Handle parse errors
chrisrrowland Aug 18, 2021
372aa34
Show an error if can't load sections from canvas
chrisrrowland Aug 18, 2021
f70a79d
Fix regex
chrisrrowland Aug 19, 2021
17f2221
Multiselect support
chrisrrowland Aug 19, 2021
5b9b228
Get rid of some unnecessary state
chrisrrowland Aug 19, 2021
78d770d
Select newly created section
chrisrrowland Aug 19, 2021
3328e6b
Changed uniqname references
chrisrrowland Aug 19, 2021
d82d2d8
Merge branch 'main' into 107_bulk_enroll_um_user
chrisrrowland Aug 25, 2021
6916160
Make cancel button functional
chrisrrowland Aug 25, 2021
63177d7
remove some log statements
chrisrrowland Aug 25, 2021
3c1ccc5
Preemptive assault on variable names
chrisrrowland Aug 25, 2021
8ae9f7e
Fix error icon color
chrisrrowland Aug 25, 2021
3a81cc0
Added missing newline
chrisrrowland Aug 25, 2021
56ef565
Search within sections
chrisrrowland Aug 27, 2021
cb4a9f4
Add clear button
chrisrrowland Aug 27, 2021
2599008
A little room to breathe above section selctor
chrisrrowland Aug 27, 2021
de43bad
Cleaner clear button on section search
chrisrrowland Aug 27, 2021
b6a212b
Fix Role being labled as Section Name
chrisrrowland Aug 27, 2021
397c243
Section subtext 'students' instead of 'users'
chrisrrowland Aug 27, 2021
23a5240
Add a small margin below section list
chrisrrowland Aug 27, 2021
961d4ff
Don't sort enrollments
chrisrrowland Aug 30, 2021
ca0c984
Fix wrong line number in error
chrisrrowland Aug 30, 2021
f2d8e17
remove log statement
chrisrrowland Aug 30, 2021
af0c7bb
Fix floating swagger link
chrisrrowland Aug 30, 2021
c1f0b29
Fix non functional spelling error
chrisrrowland Aug 30, 2021
4180339
Use enums in switch statement.
chrisrrowland Aug 30, 2021
cf854b5
Remove a comment
chrisrrowland Aug 30, 2021
2e82a29
change bulk um enroll example file name
chrisrrowland Aug 30, 2021
79f8f3d
never ever have a line of currently unreferenced code
chrisrrowland Aug 31, 2021
adb5d08
Spacing between section name input and button
chrisrrowland Aug 31, 2021
7caf827
Change hint text
chrisrrowland Aug 31, 2021
6cc9179
Add helpful tooltip
chrisrrowland Aug 31, 2021
278400d
Make 'Or select one available section to add users' farther away
chrisrrowland Aug 31, 2021
45854c8
improve responsiveness of create section widget
chrisrrowland Aug 31, 2021
6a2abdf
New tooltip text
chrisrrowland Aug 31, 2021
485fcb9
Change the name again. I'm sure it wont change again 🤪
chrisrrowland Aug 31, 2021
bb3df7e
Toast on parsing error
chrisrrowland Aug 31, 2021
477e774
So I can put this back in again later
chrisrrowland Aug 31, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useState } from 'react'
import ConfirmationTable from './ConfirmationTable'

interface IAddUMUserEnrollment {
rowNumber: number
loginID: string
role: string
}

interface BulkEnrollUMUserConfirmationTableProps {
enrollments: IAddUMUserEnrollment[]
}

interface TableHeaderColumnInfoShouldUseMatUIType {
id: keyof IAddUMUserEnrollment
label: string
minWidth: number
align?: 'left' | 'right' | undefined
}

const columns: TableHeaderColumnInfoShouldUseMatUIType[] = [
{ id: 'rowNumber', label: 'Row Number', minWidth: 25 },
{ id: 'loginID', label: 'Login ID', minWidth: 100 },
{ id: 'role', label: 'Role', minWidth: 100 }
]

function BulkEnrollUMUserConfirmationTable (props: BulkEnrollUMUserConfirmationTableProps): JSX.Element {
const [page, setPage] = useState<number>(0)

const tableRows = props.enrollments.sort((a, b) => a.loginID.localeCompare(b.loginID))

return <ConfirmationTable<IAddUMUserEnrollment> {...{ tableRows, columns, page, setPage }} />
}

export type { BulkEnrollUMUserConfirmationTableProps, IAddUMUserEnrollment }
export default BulkEnrollUMUserConfirmationTable
101 changes: 101 additions & 0 deletions ccm_web/client/src/components/CreateSectionWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Button, Grid, makeStyles, TextField } from '@material-ui/core'
import React, { ChangeEvent, useState } from 'react'
import { useSnackbar } from 'notistack'
import { addCourseSections } from '../api'
import { CanvasCourseSection } from '../models/canvas'
import { CCMComponentProps } from '../models/FeatureUIData'
import { CanvaCoursesSectionNameValidator, ICanvasSectionNameInvalidError } from '../utils/canvasSectionNameValidator'
import { CODE_NUMPAD_ENTER, CODE_RETURN } from 'keycode-js'

const useStyles = makeStyles((theme) => ({
root: {
backgroundColor: '#FAFAFA',
height: 200
},
input: {
width: '100%',
paddingRight: '2px'
},
button: {
width: '100%'
}
}))

export interface CreateSectionWidgetProps extends CCMComponentProps {
onSectionCreated: (newSection: CanvasCourseSection) => void
}

function CreateSectionWidget (props: CreateSectionWidgetProps): JSX.Element {
const classes = useStyles()
const { enqueueSnackbar } = useSnackbar()
const [newSectionName, setNewSectionName] = useState<string>('')
const [isCreating, setIsCreating] = useState(false)
const nameValidator = new CanvaCoursesSectionNameValidator(props.globals.course)

const newSectionNameChanged = (event: ChangeEvent<HTMLInputElement>): void => {
setNewSectionName(event.target.value)
}

const errorAlert = (errors: ICanvasSectionNameInvalidError[]): void => {
const errorMessage = errors.map(e => { return e.reason }).join('<br/>')
enqueueSnackbar(errorMessage, {
variant: 'error'
})
}

const createSection = (): void => {
if (newSectionName.trim().length === 0) {
return
}
setIsCreating(true)
nameValidator.validateSectionName(newSectionName).then(errors => {
if (errors.length === 0) {
addCourseSections(props.globals.course.id, [newSectionName])
.then(newSections => {
props.onSectionCreated(newSections[0])
setNewSectionName('')
}).catch(() => {
enqueueSnackbar('Error adding section', {
variant: 'error'
})
})
} else {
errorAlert(errors)
}
}).catch(() => {
enqueueSnackbar('Error validating section name', {
variant: 'error'
})
}).finally(() => {
setIsCreating(false)
})
}

const isCreateDisabled = (): boolean => {
return isCreating || newSectionName.trim().length === 0
}

const keyDown = (code: string): void => {
if (isCreateDisabled()) return
if (code === CODE_RETURN || code === CODE_NUMPAD_ENTER) {
createSection()
}
}

return (
<>
<Grid container>
<Grid item xs={9}>
<TextField className={classes.input} size='small' label='New Section Name' variant='outlined' id="outlined-basic" onChange={newSectionNameChanged} value={newSectionName} onKeyDown={(e) => keyDown(e.code)}/>
</Grid>
<Grid item>
<Button className={classes.button} variant="contained" color="primary" onClick={createSection} value={newSectionName} disabled={isCreateDisabled()}>
Create
</Button>
chrisrrowland marked this conversation as resolved.
Show resolved Hide resolved
</Grid>
</Grid>
</>
)
}

export default CreateSectionWidget
98 changes: 98 additions & 0 deletions ccm_web/client/src/components/SectionSelectorWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Grid, List, ListItem, ListItemText, makeStyles, TextField } from '@material-ui/core'
import ClearIcon from '@material-ui/icons/Clear'
import React, { useEffect, useState } from 'react'
import { CanvasCourseSection } from '../models/canvas'

const useStyles = makeStyles((theme) => ({
listContainer: {
overflow: 'auto',
marginBottom: '5px'
},
searchContainer: {
textAlign: 'left'
},
searchTextField: {
width: '100%'
}
}))

interface ISectionSelectorWidgetProps {
sections: CanvasCourseSection[]
selectedSections: CanvasCourseSection[]
height: number
multiSelect: boolean
selectionUpdated: (section: CanvasCourseSection[]) => void
}

function SectionSelectorWidget (props: ISectionSelectorWidgetProps): JSX.Element {
const classes = useStyles()

const [sectionFilterText, setSectionFilterText] = useState<string>('')
const [filteredSections, setFilteredSections] = useState<CanvasCourseSection[]>(props.sections)

useEffect(() => {
if (sectionFilterText.length === 0) {
setFilteredSections(props.sections)
} else {
setFilteredSections(props.sections.filter(p => { return p.name.toUpperCase().includes(sectionFilterText.toUpperCase()) }))
}
}, [sectionFilterText, props.sections])

const handleListItemClick = (
sectionId: number
): void => {
let newSelections = [...props.selectedSections]
const alreadySelected = props.selectedSections.filter(s => { return s.id === sectionId })
if (alreadySelected.length > 0) {
newSelections.splice(newSelections.indexOf(alreadySelected[0]), 1)
} else {
if (props.multiSelect) {
newSelections.push(filteredSections.filter(s => { return s.id === sectionId })[0])
} else {
newSelections = [filteredSections.filter(s => { return s.id === sectionId })[0]]
}
}
props.selectionUpdated(newSelections)
}

const isSectionSelected = (sectionId: number): boolean => {
return props.selectedSections.map(s => { return s.id }).includes(sectionId)
}

const searchChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setSectionFilterText(event.target.value)
}

const clearSearch = (): void => {
setSectionFilterText('')
}

const getSearchTextFieldEndAdornment = (hasText: boolean): JSX.Element => {
if (!hasText) {
return (<></>)
} else {
return (<ClearIcon onClick={clearSearch}/>)
}
}
// Passing in the height in the props seems like the wrong solution, but wanted to move on from solving that for now
return (
<>
<Grid container>
<Grid item container className={classes.searchContainer} xs={12}>
<TextField className={classes.searchTextField} onChange={searchChange} value={sectionFilterText} id='textField_Search' size='small' label='Search Sections' variant='outlined' InputProps={{ endAdornment: getSearchTextFieldEndAdornment(sectionFilterText.length > 0) }}/>
</Grid>
<Grid item xs={12}>
<List className={classes.listContainer} style={{ maxHeight: props.height }}>
{filteredSections.map((section, index) => {
return (<ListItem divider key={section.id} button selected={isSectionSelected(section.id)} onClick={(event) => handleListItemClick(section.id)}>
<ListItemText primary={section.name} secondary={`${section.total_students ?? '?'} students`}></ListItemText>
</ListItem>)
})}
</List>
</Grid>
</Grid>
</>
)
}

export default SectionSelectorWidget
3 changes: 2 additions & 1 deletion ccm_web/client/src/models/FeatureUIData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FeatureDataProps, mergeSectionProps, canvasGradebookFormatterProps, Ext
import ConvertCanvasGradebook from '../pages/GradebookCanvas'
import MergeSections from '../pages/MergeSections'
import BulkSectionCreate from '../pages/BulkSectionCreate'
import AddUMUsers from '../pages/AddUMUsers'
import { Globals, RoleEnum } from './models'

export interface CCMComponentProps {
Expand Down Expand Up @@ -61,7 +62,7 @@ const createSectionsCardProps: FeatureUIProps = {
const addUMUsersCardProps: FeatureUIProps = {
data: addUMUsersProps,
icon: <PersonAddIcon fontSize='large' />,
component: MergeSections,
component: AddUMUsers,
route: '/add-um-users'
}

Expand Down
15 changes: 14 additions & 1 deletion ccm_web/client/src/models/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,18 @@ export interface CanvasCourseBase {
export interface CanvasCourseSection {
id: number
name: string
total_students?: number
total_students: number
}

export interface CanvasRole {
clientName: string
canvasName: string
}

export const canvasRoles: CanvasRole[] = [
pushyamig marked this conversation as resolved.
Show resolved Hide resolved
{ clientName: 'student', canvasName: 'StudentEnrollment' },
{ clientName: 'teacher', canvasName: 'TeacherEnrollment' },
{ clientName: 'ta', canvasName: 'TaEnrollment' },
{ clientName: 'observer', canvasName: 'ObserverEnrollment' },
{ clientName: 'designer', canvasName: 'DesignerEnrollment' }
]
2 changes: 2 additions & 0 deletions ccm_web/client/src/models/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ export interface APIErrorData {
export interface IDefaultError {
errors: APIErrorPayload[]
}

export const UNIQNAME_REGEX = '^[a-zA-Z]{3,8}$'
chrisrrowland marked this conversation as resolved.
Show resolved Hide resolved
Loading