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

ManageStudentsTable: save all functional #20954

Merged
merged 8 commits into from Mar 2, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/i18n/common/en_us.json
Expand Up @@ -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.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

"manageStudentsNotificationSuccess": "Success!",
Copy link
Contributor

Choose a reason for hiding this comment

The 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",
Expand Down
Expand Up @@ -4,7 +4,7 @@ import PopUpMenu, {MenuBreak} from "@cdo/apps/lib/ui/PopUpMenu";
import color from "../../util/color";
import FontAwesome from '../FontAwesome';
import Button from '../Button';
import {startEditingStudent, cancelEditingStudent, removeStudent, saveStudent, addStudent, RowType} from './manageStudentsRedux';
import {startEditingStudent, cancelEditingStudent, removeStudent, saveStudent, addStudents, RowType} from './manageStudentsRedux';
import {connect} from 'react-redux';
import BaseDialog from '../BaseDialog';
import DialogFooter from "../teacherDashboard/DialogFooter";
Expand Down Expand Up @@ -180,6 +180,6 @@ export default connect(state => ({}), dispatch => ({
dispatch(saveStudent(id));
},
addStudent(id) {
dispatch(addStudent(id));
dispatch(addStudents([id]));
},
}))(ManageStudentActionsCell);
19 changes: 12 additions & 7 deletions apps/src/templates/manageStudents/ManageStudentsTable.jsx
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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()}
/>
Expand Down Expand Up @@ -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}
/>
}
Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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);

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

148 changes: 85 additions & 63 deletions apps/src/templates/manageStudents/manageStudentsRedux.js
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Why doesn't it have these keys in its initial state?
  2. This is a thing Immutable.js is really good at 😁 but not sure it's worth pivoting right now.

const initialState = {
loginType: '',
studentData: {},
editingData: {},
sectionId: null,
addStatus: {},
};

const SET_LOGIN_TYPE = 'manageStudents/SET_LOGIN_TYPE';
Expand Down Expand Up @@ -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) => {
Expand All @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we plan to change this later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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));
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: This is a long line, I'd break it across your chained method calls.

const newStudentsToAdd = arrayOfEditedData
  .filter(student => student.rowType === RowType.NEW_STUDENT)
  .map(student => student.id);

...also
combo_super_combo_finish

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 = {};
Expand Down Expand Up @@ -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}
};
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return newState;
}
if (action.type === EDIT_STUDENT) {
return {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a map operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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) => {
Expand Down