Skip to content

Commit

Permalink
Students form & students list
Browse files Browse the repository at this point in the history
- Includes relation to schools
- Added table filters
- Polished modal form UI
- StudentsPage added to navigation
- Ant.design npm package updated
  • Loading branch information
BenjaVR committed Jan 9, 2019
1 parent e120b5b commit e5087e9
Show file tree
Hide file tree
Showing 16 changed files with 642 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .firebaserc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"projects": {
"default": "stud-plan"
}
}
}
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"antd": "3.11.4",
"antd": "3.12.1",
"classnames": "2.2.6",
"core-js": "2.6.1",
"firebase": "5.7.1",
Expand Down
3 changes: 1 addition & 2 deletions web/src/components/educations/EducationsFormModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class EducationFormModal extends React.Component<EducationFormModalProps, IEduca
destroyOnClose={true}
>
<Form>
<FormItem>
<FormItem label="Naam">
{getFieldDecorator<IEducation>("name", {
validateTrigger: this.state.formValidateTrigger,
rules: [
Expand All @@ -69,7 +69,6 @@ class EducationFormModal extends React.Component<EducationFormModalProps, IEduca
})(
<Input
autoFocus={true}
placeholder="Naam"
disabled={this.state.isSubmitting}
/>,
)}
Expand Down
3 changes: 1 addition & 2 deletions web/src/components/schools/SchoolsFormModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class SchoolsFormModal extends React.Component<SchoolFormModalProps, ISchoolsFor
destroyOnClose={true}
>
<Form>
<FormItem>
<FormItem label="Naam">
{getFieldDecorator<ISchool>("name", {
validateTrigger: this.state.formValidateTrigger,
rules: [
Expand All @@ -69,7 +69,6 @@ class SchoolsFormModal extends React.Component<SchoolFormModalProps, ISchoolsFor
})(
<Input
autoFocus={true}
placeholder="Naam"
disabled={this.state.isSubmitting}
/>,
)}
Expand Down
158 changes: 158 additions & 0 deletions web/src/components/students/StudentsFormModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Form, Input, Modal, Select } from "antd";
import { FormComponentProps } from "antd/lib/form";
import FormItem from "antd/lib/form/FormItem";
import React from "react";
import { FormValidationTrigger } from "../../helpers/types";
import { ISchool } from "../../models/School";
import { IStudent } from "../../models/Student";

interface IStudentsFormModalProps {
title: string;
okText: string;
isVisible: boolean;
studentToEdit: IStudent | undefined;
schools: ISchool[];
isLoadingSchools: boolean;
onCloseRequest: () => void;
submitStudent(student: IStudent): Promise<void>;
}

type StudentFormModalProps = IStudentsFormModalProps & FormComponentProps;

interface IStudentsFormModalState {
isSubmitting: boolean;
formValidateTrigger: FormValidationTrigger;
}

class StudentsFormModal extends React.Component<StudentFormModalProps, IStudentsFormModalState> {

constructor(props: StudentFormModalProps) {
super(props);

this.state = {
isSubmitting: false,
formValidateTrigger: "",
};

this.doClose = this.doClose.bind(this);
this.handleOk = this.handleOk.bind(this);
}

public componentDidUpdate(prevProps: StudentFormModalProps): void {
// Populate the form if it is just opened and if a student to edit is passed.
if (this.props.studentToEdit !== undefined
&& this.props.isVisible === true
&& prevProps.isVisible === false) {

let schoolId;
const studentSchoolId = this.props.studentToEdit.schoolId;
if (studentSchoolId !== undefined) {
schoolId = this.props.schools.some((s) => s.id === studentSchoolId)
? studentSchoolId
: undefined;
}

this.props.form.setFieldsValue({ // TODO: fill these keys dynamically
firstName: this.props.studentToEdit.firstName,
lastName: this.props.studentToEdit.lastName,
schoolId,
});
}
}

public render(): React.ReactNode {
const { getFieldDecorator } = this.props.form;

return (
<Modal
visible={this.props.isVisible}
title={this.props.title}
onCancel={this.doClose}
onOk={this.handleOk}
okText={this.props.okText}
confirmLoading={this.state.isSubmitting}
destroyOnClose={true}
>
<Form layout="horizontal">
<FormItem label="Voornaam">
{getFieldDecorator<IStudent>("firstName", {
validateTrigger: this.state.formValidateTrigger,
rules: [
{ required: true, message: "Voornaam mag niet leeg zijn" },
],
})(
<Input
autoFocus={true}
disabled={this.state.isSubmitting}
/>,
)}
</FormItem>
<FormItem label="Familienaam">
{getFieldDecorator<IStudent>("lastName", {
validateTrigger: this.state.formValidateTrigger,
})(
<Input
disabled={this.state.isSubmitting}
/>,
)}
</FormItem>
<FormItem label="School">
{getFieldDecorator<IStudent>("schoolId", {
validateTrigger: this.state.formValidateTrigger,
})(
<Select
disabled={this.state.isSubmitting}
loading={this.props.isLoadingSchools}
allowClear={true}
>
{this.props.schools.map(this.renderSchoolSelectOption)}
</Select>,
)}
</FormItem>
</Form>
</Modal>
);
}

private renderSchoolSelectOption(school: ISchool): React.ReactNode {
return (
<Select.Option key={school.id} value={school.id}>{school.name}</Select.Option>
);
}

private doClose(): void {
this.props.onCloseRequest();
}

private handleOk(): void {
// Do real-time validation (while typing) only after the first submit.
this.setState({
formValidateTrigger: "onChange",
});

const fields: Array<keyof IStudent> = ["firstName", "lastName", "schoolId"];
this.props.form.validateFieldsAndScroll(fields, (errors, values) => {
if (!errors) {
this.setState({
isSubmitting: true,
});

const student: IStudent = {
...values,
};
this.props.submitStudent(student)
.then(() => {
this.doClose();
})
.finally(() => {
this.setState({
isSubmitting: false,
});
});
}
});
}
}

const WrappedStudentsFormModal = Form.create<IStudentsFormModalProps>()(StudentsFormModal);
export default WrappedStudentsFormModal;
181 changes: 181 additions & 0 deletions web/src/components/students/StudentsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { Col, notification, Row } from "antd";
import React from "react";
import { ISchool } from "../../models/School";
import { IStudent } from "../../models/Student";
import { RoutePageComponentProps, routes } from "../../routes";
import { SchoolsService } from "../../services/SchoolsService";
import { StudentsService } from "../../services/StudentsService";
import AuthenticatedLayout from "../layouts/AuthenticatedLayout";
import StudentFormModal from "./StudentsFormModal";
import StudentsTable from "./StudentsTable";

type StudentsPageProps = RoutePageComponentProps;

interface IStudentsPageState {
students: IStudent[];
isFetching: boolean;
isAddStudentsModalVisible: boolean;
isEditStudentsModalVisible: boolean;
studentToEdit: IStudent | undefined;
schools: ISchool[];
isFetchingSchools: boolean;
}

export default class StudentsPage extends React.Component<StudentsPageProps, IStudentsPageState> {

private readonly studentsService = new StudentsService();
private readonly schoolsService = new SchoolsService();

constructor(props: StudentsPageProps) {
super(props);

this.state = {
students: [],
isFetching: true,
isAddStudentsModalVisible: false,
isEditStudentsModalVisible: false,
studentToEdit: undefined,
schools: [],
isFetchingSchools: true,
};

this.openAddStudentModal = this.openAddStudentModal.bind(this);
this.closeAddStudentModal = this.closeAddStudentModal.bind(this);
this.openEditStudentModal = this.openEditStudentModal.bind(this);
this.closeEditStudentModal = this.closeEditStudentModal.bind(this);
this.addStudent = this.addStudent.bind(this);
this.editStudent = this.editStudent.bind(this);
this.deleteStudent = this.deleteStudent.bind(this);
}

public componentDidMount(): void {
this.studentsService.subscribe((students) => {
this.setState({
isFetching: false,
students,
});
});
this.schoolsService.subscribe((schools) => {
this.setState({
isFetchingSchools: false,
schools,
});
});
}

public render(): React.ReactNode {
return (
<React.Fragment>
<AuthenticatedLayout router={{ history: this.props.history }} initialRoute={routes.studentsRoute}>
<Row>
<Col>
<StudentsTable
isLoading={this.state.isFetching}
students={this.state.students}
deleteStudent={this.deleteStudent}
onAddStudentRequest={this.openAddStudentModal}
onEditStudentRequest={this.openEditStudentModal}
schools={this.state.schools}
isLoadingSchools={this.state.isFetchingSchools}
/>
</Col>
</Row>
</AuthenticatedLayout>
<StudentFormModal
title="Nieuwe student toevoegen"
okText="Voeg toe"
isVisible={this.state.isAddStudentsModalVisible}
submitStudent={this.addStudent}
onCloseRequest={this.closeAddStudentModal}
studentToEdit={undefined}
schools={this.state.schools}
isLoadingSchools={this.state.isFetchingSchools}
/>
<StudentFormModal
title="Student bewerken"
okText="Bewerk"
isVisible={this.state.isEditStudentsModalVisible}
submitStudent={this.editStudent}
onCloseRequest={this.closeEditStudentModal}
studentToEdit={this.state.studentToEdit}
schools={this.state.schools}
isLoadingSchools={this.state.isFetchingSchools}
/>
</React.Fragment>
);
}

private openAddStudentModal(): void {
this.setState({ isAddStudentsModalVisible: true });
}

private closeAddStudentModal(): void {
this.setState({ isAddStudentsModalVisible: false });
}

private openEditStudentModal(student: IStudent): void {
this.setState({
isEditStudentsModalVisible: true,
studentToEdit: student,
});
}

private closeEditStudentModal(): void {
this.setState({ isEditStudentsModalVisible: false });
}

private addStudent(student: IStudent): Promise<void> {
return new Promise<void>((resolve, reject): void => {
this.studentsService.add(student)
.then(() => {
notification.success({
message: `Student "${student.firstName}" succesvol toegevoegd`,
});
resolve();
})
.catch((error) => {
notification.error({
message: "Kon student niet toevoegen",
});
reject(error);
});
});
}

private editStudent(student: IStudent): Promise<void> {
student.id = this.state.studentToEdit!.id;
return new Promise<void>((resolve, reject): void => {
this.studentsService.update(student)
.then(() => {
notification.success({
message: `Student "${student.firstName}" succesvol bewerkt`,
});
resolve();
})
.catch((error) => {
notification.error({
message: "Kon student niet bewerken",
});
reject(error);
});
});
}

private deleteStudent(student: IStudent): Promise<void> {
return new Promise<void>((resolve, reject): void => {
this.studentsService.delete(student)
.then(() => {
notification.success({
message: `Student "${student.firstName}" succesvol verwijderd`,
});
resolve();
})
.catch((error) => {
notification.error({
message: `Kon student "${student.firstName}" niet verwijderen, probeer later opnieuw`,
});
reject(error);
});
});
}
}
Loading

2 comments on commit e5087e9

@BenjaVR
Copy link
Owner

@BenjaVR BenjaVR commented on e5087e9 Jan 9, 2019

Choose a reason for hiding this comment

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

#17

@BenjaVR
Copy link
Owner

@BenjaVR BenjaVR commented on e5087e9 Jan 9, 2019

Choose a reason for hiding this comment

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

#18

Please sign in to comment.