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
ManageStudentsTable: save all functional #20954
Changes from 6 commits
748f523
0fada6c
752aece
882d5a4
fce9965
29f6fee
89fa254
7520107
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -618,8 +618,8 @@ | |
"manageAssets": "Manage Assets", | ||
"manageStudents": "Manage Students", | ||
"manageStudentsNotificationFailure": "Something went wrong.", | ||
"manageStudentsNotificationCannotAdd": "You could not add student to your section. Please try again or refresh the page.", | ||
"manageStudentsNotificationAddSuccess": "You added 1 student to your section.", | ||
"manageStudentsNotificationCannotAdd": "You could not add {numStudents, plural, one {1 student} other {# students}} to your section. Please try again or refresh the page.", | ||
"manageStudentsNotificationAddSuccess": "You added {numStudents, plural, one {1 student} other {# students}} to your section.", | ||
"manageStudentsNotificationSuccess": "Success!", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tiny nit: Do we want this string to have "student(s)" for cases when numStudents is 1? |
||
"map": "Map", | ||
"minecraft2017Button": "Go to Minecraft Education", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ import ManageStudentsNameCell from './ManageStudentsNameCell'; | |
import ManageStudentsAgeCell from './ManageStudentsAgeCell'; | ||
import ManageStudentsGenderCell from './ManageStudentsGenderCell'; | ||
import ManageStudentsActionsCell from './ManageStudentsActionsCell'; | ||
import {convertStudentDataToArray, AddStatus, RowType} from './manageStudentsRedux'; | ||
import {convertStudentDataToArray, AddStatus, RowType, saveAllStudents} from './manageStudentsRedux'; | ||
import { connect } from 'react-redux'; | ||
import Notification, {NotificationType} from '../Notification'; | ||
import AddMultipleStudents from './AddMultipleStudents'; | ||
|
@@ -100,7 +100,8 @@ class ManageStudentsTable extends Component { | |
studentData: PropTypes.arrayOf(studentSectionDataPropType), | ||
loginType: PropTypes.string, | ||
editingData: PropTypes.object, | ||
addStatus: PropTypes.oneOf(Object.values(AddStatus)), | ||
addStatus: PropTypes.object, | ||
saveAllStudents: PropTypes.func, | ||
}; | ||
|
||
state = { | ||
|
@@ -169,7 +170,7 @@ class ManageStudentsTable extends Component { | |
<div> | ||
{numberOfEditingRows > 1 && | ||
<Button | ||
onClick={()=>{console.log("save all");}} | ||
onClick={this.props.saveAllStudents} | ||
color={Button.ButtonColor.orange} | ||
text={i18n.saveAll()} | ||
/> | ||
|
@@ -333,19 +334,19 @@ class ManageStudentsTable extends Component { | |
|
||
return ( | ||
<div> | ||
{addStatus === AddStatus.SUCCESS && | ||
{addStatus.status === AddStatus.SUCCESS && | ||
<Notification | ||
type={NotificationType.success} | ||
notice={i18n.manageStudentsNotificationSuccess()} | ||
details={i18n.manageStudentsNotificationAddSuccess()} | ||
details={i18n.manageStudentsNotificationAddSuccess({numStudents: addStatus.numStudents})} | ||
dismissible={false} | ||
/> | ||
} | ||
{addStatus === AddStatus.FAIL && | ||
{addStatus.status === AddStatus.FAIL && | ||
<Notification | ||
type={NotificationType.failure} | ||
notice={i18n.manageStudentsNotificationFailure()} | ||
details={i18n.manageStudentsNotificationCannotAdd()} | ||
details={i18n.manageStudentsNotificationCannotAdd({numStudents: addStatus.numStudents})} | ||
dismissible={false} | ||
/> | ||
} | ||
|
@@ -371,4 +372,8 @@ export default connect(state => ({ | |
studentData: convertStudentDataToArray(state.manageStudents.studentData), | ||
editingData: state.manageStudents.editingData, | ||
addStatus: state.manageStudents.addStatus, | ||
}), dispatch => ({ | ||
saveAllStudents() { | ||
dispatch(saveAllStudents()); | ||
}, | ||
}))(ManageStudentsTable); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good place for the propsFromDispatch shorthand: export default connect(state => ({
loginType: state.manageStudents.loginType,
// Other propsFromState stuff...
}), {
// If you just pass an object of action creators as the second argument to connect,
// react-redux assumes they're all simple pass-throughs like this:
// saveAllStudents: (...args) => dispatch(saveAllStudents(...args)),
saveAllStudents,
})(ManageStudentsTable); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to leave this one as it. I find it confusing to understand what's going without the explicit call. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,11 +51,16 @@ const blankNewStudentRow = { | |
}; | ||
|
||
// Initial state for the manageStudents redux store. | ||
|
||
// addStatus should be an object with the following keys | ||
// status - AddStatus enum describing the result of the add | ||
// numStudents - number of students that were attempted to add | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const initialState = { | ||
loginType: '', | ||
studentData: {}, | ||
editingData: {}, | ||
sectionId: null, | ||
addStatus: {}, | ||
}; | ||
|
||
const SET_LOGIN_TYPE = 'manageStudents/SET_LOGIN_TYPE'; | ||
|
@@ -84,8 +89,12 @@ export const setSecretWords = (studentId, words) => ({ type: SET_SECRET_WORDS, s | |
export const editStudent = (studentId, studentData) => ({ type: EDIT_STUDENT, studentId, studentData }); | ||
export const startSavingStudent = (studentId) => ({ type: START_SAVING_STUDENT, studentId }); | ||
export const saveStudentSuccess = (studentId) => ({ type: SAVE_STUDENT_SUCCESS, studentId }); | ||
export const addStudentSuccess = (rowId, studentData) => ({ type: ADD_STUDENT_SUCCESS, rowId, studentData }); | ||
export const addStudentFailure = (error, studentId) => ({ type: ADD_STUDENT_FAILURE, error, studentId }); | ||
export const addStudentsSuccess = (numStudents, rowIds, studentData) => ( | ||
{ type: ADD_STUDENT_SUCCESS, numStudents, rowIds, studentData } | ||
); | ||
export const addStudentsFailure = (numStudents, error, studentIds) => ( | ||
{ type: ADD_STUDENT_FAILURE, numStudents, error, studentIds } | ||
); | ||
export const addMultipleRows = (studentData) => ({ type: ADD_MULTIPLE_ROWS, studentData }); | ||
|
||
export const saveStudent = (studentId) => { | ||
|
@@ -101,24 +110,57 @@ export const saveStudent = (studentId) => { | |
}; | ||
}; | ||
|
||
// Adds a student, with the given row id (studentId), from an addRow or | ||
// a newStudentRow. | ||
export const addStudent = (studentId) => { | ||
// Saves all RowType.STUDENT currently being edited and adds all | ||
// RowType.NEW_STUDENT currently being edited. | ||
export const saveAllStudents = () => { | ||
return (dispatch, getState) => { | ||
const state = getState().manageStudents; | ||
dispatch(startSavingStudent(studentId)); | ||
addStudentOnServer(state.editingData[studentId], state.sectionId, (error, data) => { | ||
|
||
// Currently, every update is an individual call to the server. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we plan to change this later? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe for V2. I think it could be useful to think about changing the API once we create the "edit all" feature. |
||
const currentlyEditedData = convertStudentDataToArray(state.editingData); | ||
let studentsToSave = currentlyEditedData.filter(student => student.rowType === RowType.STUDENT); | ||
for (let i = 0; i<studentsToSave.length; i++) { | ||
dispatch(saveStudent(studentsToSave[i].id)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: My personal preference for straightforward iterations like this is studentsToSave.forEach(student => dispatch(saveStudent(student.id))); |
||
} | ||
|
||
// Adding students can be saved together. | ||
const arrayOfEditedData = convertStudentDataToArray(state.editingData); | ||
const newStudentsToAdd = arrayOfEditedData.filter(student => student.rowType === RowType.NEW_STUDENT).map( | ||
student => student.id); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (newStudentsToAdd.length > 0) { | ||
dispatch(addStudents(newStudentsToAdd)); | ||
} | ||
}; | ||
}; | ||
|
||
// Adds a student, with the given row id (studentIds), from RowType.ADD or | ||
// RowType.NEW_STUDENT. | ||
export const addStudents = (studentIds) => { | ||
return (dispatch, getState) => { | ||
const state = getState().manageStudents; | ||
const numStudentsToAdd = studentIds.length; | ||
|
||
// Update each row to saving in progress. | ||
for (let i = 0; i<studentIds.length; i++) { | ||
dispatch(startSavingStudent(studentIds[i])); | ||
} | ||
|
||
const arrayOfEditedData = convertStudentDataToArray(state.editingData); | ||
const filteredData = arrayOfEditedData.filter(student => studentIds.includes(student.id)); | ||
addStudentOnServer(filteredData, | ||
state.sectionId, (error, data) => { | ||
if (error) { | ||
dispatch(addStudentFailure(error, studentId)); | ||
dispatch(addStudentsFailure(numStudentsToAdd, error, studentIds)); | ||
console.error(error); | ||
} else { | ||
dispatch(addStudentSuccess(studentId, convertAddedStudent(data))); | ||
dispatch(addStudentsSuccess(numStudentsToAdd, studentIds, | ||
convertStudentServerData(data, state.loginType, state.sectionId))); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
// Creates a new "new student" add row for each name in the array. | ||
// Creates a new RowType.NEW_STUDENT for each name in the array. | ||
export const addMultipleAddRows = (studentNames) => { | ||
return (dispatch, getState) => { | ||
let studentData = {}; | ||
|
@@ -240,43 +282,39 @@ export default function manageStudents(state=initialState, action) { | |
}; | ||
} | ||
if (action.type === ADD_STUDENT_FAILURE) { | ||
return { | ||
let newState = { | ||
...state, | ||
studentData: { | ||
...state.studentData, | ||
[action.studentId]: { | ||
...state.studentData[action.studentId], | ||
isSaving: false, | ||
} | ||
}, | ||
addStatus: AddStatus.FAIL | ||
addStatus: {status: AddStatus.FAIL, numStudents: action.numStudents} | ||
}; | ||
for (let i = 0; i<action.studentIds.length; i++) { | ||
newState.studentData[action.studentIds[i]] = { | ||
...state.studentData[action.studentIds[i]], | ||
isSaving: false, | ||
}; | ||
} | ||
return newState; | ||
} | ||
if (action.type === ADD_STUDENT_SUCCESS) { | ||
// omit action.rowId | ||
return { | ||
let newState = { | ||
...state, | ||
studentData: { | ||
[action.studentData.id]: { | ||
...action.studentData, | ||
loginType: state.loginType, | ||
sectionId: state.sectionId, | ||
}, | ||
..._.omit(state.studentData, action.rowId), | ||
..._.omit(state.studentData, action.rowIds), | ||
...action.studentData, | ||
[addRowId]: { | ||
...blankAddRow, | ||
loginType: state.loginType | ||
}, | ||
loginType: state.loginType, | ||
} | ||
}, | ||
editingData: { | ||
..._.omit(state.editingData, action.rowId), | ||
..._.omit(state.editingData, action.rowIds), | ||
[addRowId]: { | ||
...blankAddRow, | ||
loginType: state.loginType | ||
loginType: state.loginType, | ||
} | ||
}, | ||
addStatus: AddStatus.SUCCESS, | ||
addStatus: {status: AddStatus.SUCCESS, numStudents: action.numStudents} | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When should we have spaces in braces { like this } and when should we not {like this}? I see both in our codebase, and I don't know think the styleguide has a ruling. 🤷🏼♀️ just curious There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently they are both okay: https://github.com/code-dot-org/code-dot-org/blob/staging/STYLEGUIDE.md#js-object-curly-spacing |
||
return newState; | ||
} | ||
if (action.type === EDIT_STUDENT) { | ||
return { | ||
|
@@ -353,39 +391,20 @@ export const convertStudentServerData = (studentData, loginType, sectionId) => { | |
id: student.id, | ||
name: student.name, | ||
username: student.username, | ||
age: student.age, | ||
gender: student.gender, | ||
age: student.age || '', | ||
gender: student.gender || '', | ||
secretWords: student.secret_words, | ||
secretPicturePath: student.secret_picture_path, | ||
loginType: loginType, | ||
sectionId: sectionId, | ||
isEditing: false, | ||
isSaving: false, | ||
rowType: RowType.STUDENT, | ||
}; | ||
} | ||
return studentLookup; | ||
}; | ||
|
||
// Converts added student from /v2/sections/sectionid/students to a key/value | ||
// object for the redux store | ||
export const convertAddedStudent = (studentData, loginType, sectionId) => { | ||
let student = studentData[0]; | ||
const studentObject = { | ||
id: student.id, | ||
name: student.name, | ||
username: student.username, | ||
age: student.age, | ||
gender: student.gender, | ||
secretWords: student.secret_words, | ||
secretPicturePath: student.secret_picture_path, | ||
loginType: loginType, | ||
sectionId: sectionId, | ||
isEditing: false, | ||
rowType: RowType.STUDENT, | ||
}; | ||
return studentObject; | ||
}; | ||
|
||
// Converts key/value id/student pairs to an array of student objects for the | ||
// component to display | ||
// TODO(caleybrock): memoize this - sections could be a few thousand students | ||
|
@@ -413,19 +432,22 @@ const updateStudentOnServer = (updatedStudentInfo, onComplete) => { | |
}); | ||
}; | ||
|
||
// Make a post request to add a student. | ||
const addStudentOnServer = (updatedStudentInfo, sectionId, onComplete) => { | ||
const studentToAdd = [{ | ||
editing: true, | ||
name: updatedStudentInfo.name, | ||
age: updatedStudentInfo.age, | ||
gender: updatedStudentInfo.gender, | ||
}]; | ||
// Make a post request to add students. | ||
const addStudentOnServer = (updatedStudentsInfo, sectionId, onComplete) => { | ||
const studentsToAdd = []; | ||
for (let i = 0; i<updatedStudentsInfo.length; i++) { | ||
studentsToAdd[i] = { | ||
editing: true, | ||
name: updatedStudentsInfo[i].name, | ||
age: updatedStudentsInfo[i].age, | ||
gender: updatedStudentsInfo[i].gender, | ||
}; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops missed this one - but will follow up. |
||
$.ajax({ | ||
url: `/v2/sections/${sectionId}/students`, | ||
method: 'POST', | ||
contentType: 'application/json;charset=UTF-8', | ||
data: JSON.stringify(studentToAdd), | ||
data: JSON.stringify(studentsToAdd), | ||
}).done((data) => { | ||
onComplete(null, data); | ||
}).fail((jqXhr, status) => { | ||
|
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.