-
Notifications
You must be signed in to change notification settings - Fork 479
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22060 from code-dot-org/move-students-react
Write 'move students' functionality in React
- Loading branch information
Showing
4 changed files
with
350 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
import React, {Component, PropTypes} from 'react'; | ||
import i18n from "@cdo/locale"; | ||
import {Table, sort} from 'reactabular'; | ||
import wrappedSortable from '../tables/wrapped_sortable'; | ||
import {tableLayoutStyles, sortableOptions} from "../tables/tableConstants"; | ||
import Immutable from 'immutable'; | ||
import orderBy from 'lodash/orderBy'; | ||
import Button from '../Button'; | ||
import BaseDialog from '../BaseDialog'; | ||
import DialogFooter from "../teacherDashboard/DialogFooter"; | ||
|
||
const PADDING = 20; | ||
const TABLE_WIDTH = 300; | ||
const CHECKBOX_CELL_WIDTH = 50; | ||
|
||
const styles = { | ||
dialog: { | ||
paddingLeft: PADDING, | ||
paddingRight: PADDING, | ||
paddingTop: PADDING, | ||
paddingBottom: PADDING | ||
}, | ||
table: { | ||
width: TABLE_WIDTH | ||
}, | ||
checkboxCell: { | ||
width: CHECKBOX_CELL_WIDTH, | ||
textAlign: 'center' | ||
}, | ||
checkbox: { | ||
margin: 0 | ||
} | ||
}; | ||
|
||
class MoveStudents extends Component { | ||
static propTypes = { | ||
studentData: PropTypes.arrayOf( | ||
React.PropTypes.shape({ | ||
id: PropTypes.number.isRequired, | ||
name: PropTypes.string.isRequired, | ||
}) | ||
).isRequired | ||
}; | ||
|
||
state = { | ||
isDialogOpen: false, | ||
selectedIds: [] | ||
}; | ||
|
||
openDialog = () => { | ||
this.setState({isDialogOpen: true}); | ||
}; | ||
|
||
closeDialog = () => { | ||
this.setState({ | ||
isDialogOpen: false, | ||
selectedIds: [] | ||
}); | ||
}; | ||
|
||
getStudentIds = () => { | ||
return this.props.studentData.map(s => s.id); | ||
}; | ||
|
||
areAllSelected = () => { | ||
return Immutable.Set(this.state.selectedIds).isSuperset(this.getStudentIds()); | ||
}; | ||
|
||
toggleSelectAll = () => { | ||
if (this.areAllSelected()) { | ||
this.setState({selectedIds: []}); | ||
} else { | ||
this.setState({selectedIds: this.getStudentIds()}); | ||
} | ||
}; | ||
|
||
toggleStudentSelected = (studentId) => { | ||
let selectedIds = [...this.state.selectedIds]; | ||
|
||
if (this.state.selectedIds.includes(studentId)) { | ||
const studentIndex = selectedIds.indexOf(studentId); | ||
selectedIds.splice(studentIndex, 1); | ||
} else { | ||
selectedIds.push(studentId); | ||
} | ||
|
||
this.setState({selectedIds}); | ||
}; | ||
|
||
selectedStudentHeaderFormatter = () => { | ||
return ( | ||
<input | ||
style={styles.checkbox} | ||
type="checkbox" | ||
checked={this.areAllSelected()} | ||
onChange={this.toggleSelectAll} | ||
/> | ||
); | ||
}; | ||
|
||
selectedStudentFormatter = (_, {rowData}) => { | ||
const isChecked = this.state.selectedIds.includes(rowData.id); | ||
|
||
return ( | ||
<input | ||
style={styles.checkbox} | ||
type="checkbox" | ||
checked={isChecked} | ||
onChange={() => this.toggleStudentSelected(rowData.id)} | ||
/> | ||
); | ||
}; | ||
|
||
getColumns = (sortable) => { | ||
return [ | ||
{ | ||
property: 'selected', | ||
header: { | ||
label: '', | ||
format: this.selectedStudentHeaderFormatter, | ||
props: { | ||
style: { | ||
...tableLayoutStyles.headerCell, | ||
...styles.checkboxCell | ||
}} | ||
}, | ||
cell: { | ||
format: this.selectedStudentFormatter, | ||
props: { | ||
style: { | ||
...tableLayoutStyles.cell, | ||
...styles.checkboxCell | ||
}} | ||
} | ||
}, { | ||
property: 'name', | ||
header: { | ||
label: i18n.name(), | ||
props: { | ||
style: { | ||
...tableLayoutStyles.headerCell | ||
}}, | ||
transforms: [sortable] | ||
}, | ||
cell: { | ||
props: { | ||
style: { | ||
...tableLayoutStyles.cell | ||
}} | ||
} | ||
} | ||
]; | ||
}; | ||
|
||
getSortingColumns = () => { | ||
return this.state.sortingColumns || {}; | ||
}; | ||
|
||
// The user requested a new sorting column. Adjust the state accordingly. | ||
onSort = (selectedColumn) => { | ||
this.setState({ | ||
sortingColumns: sort.byColumn({ | ||
sortingColumns: this.state.sortingColumns, | ||
// Custom sortingOrder removes 'no-sort' from the cycle | ||
sortingOrder: { | ||
FIRST: 'asc', | ||
asc: 'desc', | ||
desc: 'asc' | ||
}, | ||
selectedColumn | ||
}) | ||
}); | ||
}; | ||
|
||
render() { | ||
// Define a sorting transform that can be applied to each column | ||
const sortable = wrappedSortable(this.getSortingColumns, this.onSort, sortableOptions); | ||
const columns = this.getColumns(sortable); | ||
const sortingColumns = this.getSortingColumns(); | ||
|
||
const sortedRows = sort.sorter({ | ||
columns, | ||
sortingColumns, | ||
sort: orderBy, | ||
})(this.props.studentData); | ||
|
||
return ( | ||
<div> | ||
<Button | ||
onClick={this.openDialog} | ||
color={Button.ButtonColor.gray} | ||
text={i18n.moveStudents()} | ||
/> | ||
<BaseDialog | ||
useUpdatedStyles | ||
isOpen={this.state.isDialogOpen} | ||
style={styles.dialog} | ||
handleClose={this.closeDialog} | ||
> | ||
<Table.Provider | ||
columns={columns} | ||
style={styles.table} | ||
> | ||
<Table.Header /> | ||
<Table.Body rows={sortedRows} rowKey="id" /> | ||
</Table.Provider> | ||
<DialogFooter> | ||
<Button | ||
text={i18n.dialogCancel()} | ||
onClick={this.closeDialog} | ||
color={Button.ButtonColor.gray} | ||
/> | ||
<Button | ||
text={i18n.moveStudents()} | ||
onClick={() => {}} | ||
color={Button.ButtonColor.orange} | ||
/> | ||
</DialogFooter> | ||
</BaseDialog> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default MoveStudents; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react'; | ||
import MoveStudents from './MoveStudents'; | ||
|
||
const studentData = [ | ||
{ | ||
id: 1, | ||
name: 'Student A' | ||
}, | ||
{ | ||
id: 3, | ||
name: 'Student C' | ||
}, | ||
{ | ||
id: 2, | ||
name: 'Student B' | ||
} | ||
]; | ||
|
||
export default storybook => { | ||
storybook | ||
.storiesOf('MoveStudents', module) | ||
.addStoryTable([ | ||
{ | ||
name: 'Move students dialog', | ||
description: 'Ability to move students in a certain section to a different section or teacher', | ||
story: () => ( | ||
<MoveStudents studentData={studentData} /> | ||
) | ||
} | ||
]); | ||
}; |
93 changes: 93 additions & 0 deletions
93
apps/test/unit/templates/manageStudents/MoveStudentsTest.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import React from 'react'; | ||
import {shallow} from 'enzyme'; | ||
import {expect} from '../../../util/configuredChai'; | ||
import MoveStudents from '@cdo/apps/templates/manageStudents/MoveStudents'; | ||
|
||
const studentData = [ | ||
{id: 1, name: 'studentb'}, | ||
{id: 3, name: 'studenta'}, | ||
{id: 0, name: ''}, | ||
{id: 2, name: 'studentf'} | ||
]; | ||
|
||
describe('MoveStudents', () => { | ||
let wrapper; | ||
|
||
beforeEach(() => { | ||
wrapper = shallow(<MoveStudents studentData={studentData}/>); | ||
}); | ||
|
||
describe('#openDialog', () => { | ||
it('sets isDialogOpen state to true', () => { | ||
wrapper.instance().openDialog(); | ||
expect(wrapper.instance().state.isDialogOpen).to.equal(true); | ||
}); | ||
}); | ||
|
||
describe('#closeDialog', () => { | ||
it('sets isDialogOpen state to false', () => { | ||
wrapper.instance().isDialogOpen = true; | ||
wrapper.instance().closeDialog(); | ||
expect(wrapper.instance().state.isDialogOpen).to.equal(false); | ||
}); | ||
|
||
it('clears selectedIds in state', () => { | ||
wrapper.instance().state.selectedIds = [1,2]; | ||
expect(wrapper.instance().state.selectedIds).to.have.members([1,2]); | ||
wrapper.instance().closeDialog(); | ||
expect(wrapper.instance().state.selectedIds).to.have.members([]); | ||
}); | ||
}); | ||
|
||
describe('#getStudentIds', () => { | ||
it('returns all student ids', () => { | ||
expect(wrapper.instance().getStudentIds()).to.have.members([0,1,2,3]); | ||
}); | ||
}); | ||
|
||
describe('#areAllSelected', () => { | ||
it('returns true if all student ids are in selectedIds', () => { | ||
wrapper.instance().state.selectedIds = [0,1,2,3]; | ||
expect(wrapper.instance().areAllSelected()).to.equal(true); | ||
}); | ||
|
||
it('returns false if all student ids are not in selectedIds', () => { | ||
wrapper.instance().state.selectedIds = [0,1,2]; | ||
expect(wrapper.instance().areAllSelected()).to.equal(false); | ||
}); | ||
}); | ||
|
||
describe('#toggleSelectAll', () => { | ||
it('clears selectedIds in state if all ids are selected', () => { | ||
wrapper.instance().state.selectedIds = [0,1,2,3]; | ||
wrapper.instance().toggleSelectAll(); | ||
expect(wrapper.instance().state.selectedIds).to.have.members([]); | ||
}); | ||
|
||
it('adds all ids to selectedIds in state if some ids are selected', () => { | ||
wrapper.instance().state.selectedIds = [0,1]; | ||
wrapper.instance().toggleSelectAll(); | ||
expect(wrapper.instance().state.selectedIds).to.have.members([0,1,2,3]); | ||
}); | ||
|
||
it('adds all ids to selectedIds in state if no ids are selected', () => { | ||
wrapper.instance().state.selectedIds = []; | ||
wrapper.instance().toggleSelectAll(); | ||
expect(wrapper.instance().state.selectedIds).to.have.members([0,1,2,3]); | ||
}); | ||
}); | ||
|
||
describe('#toggleStudentSelected', () => { | ||
it('removes student id from selectedIds in state if already present', () => { | ||
wrapper.instance().state.selectedIds = [1]; | ||
wrapper.instance().toggleStudentSelected(1); | ||
expect(wrapper.instance().state.selectedIds).to.have.members([]); | ||
}); | ||
|
||
it('adds student id to selectedIds in state if not already present', () => { | ||
wrapper.instance().state.selectedIds = [1]; | ||
wrapper.instance().toggleStudentSelected(0); | ||
expect(wrapper.instance().state.selectedIds).to.have.members([0,1]); | ||
}); | ||
}); | ||
}); |