-
Notifications
You must be signed in to change notification settings - Fork 479
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
Write 'move students' functionality in React #22060
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
09fb3fb
Move AddMultipleStudents into ManageStudentsHeader
maddiedierker de51cf0
Add new 'Move students' string to i18n
maddiedierker ec68f8d
Add empty MoveStudents modal
maddiedierker 9a8146f
Pass studentData to MoveStudents component, display in sortable table
maddiedierker dac5bec
Add custom styles for students table
maddiedierker e229df7
Add 'selected' column for selecting students to be moved
maddiedierker a93c577
Add student/list toggle functionality
maddiedierker 72d0fb8
Remove any new/blank student rows before passing to ManageStudentsHeader
maddiedierker c726828
Abstract dialog padding into variable
maddiedierker 68fe965
Unit test component functions
maddiedierker 9aee47c
Add storybook for current MoveStudents functionality
maddiedierker 51edc25
Style tweaks on checkbox and cell
maddiedierker 3ce93d7
Remove ManageStudentsHeader until MoveStudents component can be added
maddiedierker bb9e58e
Remove studentDataMinusBlankRows method until needed
maddiedierker b0a9d5c
Merge branch 'staging' into move-students-react
maddiedierker 10397b0
Implement PR review feedback
maddiedierker File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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]); | ||
}); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use this in eleven places now. Let's extract it somewhere! Maybe into
wrapped_sortable.js
(which should be renamed to camelCase).