diff --git a/package.json b/package.json index fd3a75d587..70f3c13722 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "formsy-react-components": "^0.9.0", "jquery": "^3.1.1", "oauth-1.0a": "^2.0.0", + "rc-pagination": "^1.6.5", + "rc-select": "^6.7.1", "react": "^15.4.2", "react-dom": "^15.4.2", "react-dropzone": "^3.9.0", diff --git a/src/modules/administration/actions/administration.js b/src/modules/administration/actions/administration.js index 0449d945ab..e43b61c06a 100644 --- a/src/modules/administration/actions/administration.js +++ b/src/modules/administration/actions/administration.js @@ -1,42 +1,69 @@ import { Permissions, Server } from '../../core/helpers/'; -const schoolService = Server.service('/schools'); -const classService = Server.service('/classes'); -const courseService = Server.service('/courses'); const userService = Server.service('/users'); +const classService = Server.service('/classes'); + +const indexArrayByKey = (array, key) => { + const result = {}; + array.forEach((obj) => { + result[obj[key]] = obj; + }); + return result; +}; export default { - updateSchool: (data) => { - if(data._id) return schoolService.patch(data._id, data); - return schoolService.create(data); + loadContent: (serviceName, query) => { + const service = Server.service(serviceName); + return service.find({query}) + .then(result => { + return Promise.resolve({ + records: indexArrayByKey(result.data, '_id'), + pagination: {total: result.total, skip: result.skip} + }); + }); }, - - updateCourse: (data) => { - if(data._id) return courseService.patch(data._id, data); - - return courseService.create(data); + updateRecord: (serviceName, data) => { + const service = Server.service(serviceName); + if(data._id) return service.patch(data._id, data); + return service.create(data); }, - removeCourse: (data) => { - return courseService.remove(data._id); + removeRecord: (serviceName, data) => { + const id = data._id; + if(!id) throw new Error("_id not set!"); + const service = Server.service(serviceName); + return service.remove(id); }, - - updateClass: (data) => { - if(data._id) return classService.patch(data._id, data); - - return classService.create(data); + populateFields: (serviceName, _id, fields) => { + const service = Server.service(serviceName); + return service.find({query: { + _id, + $populate: fields + }}) + .then(result => Promise.resolve(result.data[0])); }, - removeClass: (data) => { - return classService.remove(data._id); + _loadTeachers: (schoolId) => { + return userService.find({ + query: { + schoolId, + roles: ['teacher'], + $limit: 1000 + } + }) + .then(result => Promise.resolve(result.data)); }, - updateUser: (data) => { - if(data._id) return userService.patch(data._id, data); - - return userService.create(data); + _loadClasses: (schoolId) => { + return classService.find({ + query: { + schoolId, + $limit: 1000 + } + }) + .then(result => Promise.resolve(result.data)); } }; diff --git a/src/modules/administration/components/admin-section.jsx b/src/modules/administration/components/admin-section.jsx index d17b901da9..08db9ab113 100644 --- a/src/modules/administration/components/admin-section.jsx +++ b/src/modules/administration/components/admin-section.jsx @@ -1,4 +1,8 @@ import ReactDOM from 'react-dom'; +import Pagination from 'rc-pagination'; +import Select from 'rc-select'; +import '../styles/rc-pagination.scss'; +import '../styles/rc-select.scss'; import ModalForm from './modal-form'; import Table from './table'; @@ -12,14 +16,27 @@ class AdminSection extends React.Component { title: '', addLabel: '', editLabel: '', - submitCallback: () => {} - } + }; this.state = { - record: {} + record: {}, + records: [], + numberOfPages: 1, + itemsPerPage: 10, }; this.defaultRecord = {}; + this.loadContentFromServer = null; + this.serviceName = null; + } + + componentDidMount() { + this.loadContent(1, this.state.itemsPerPage); + } + + // return the query passed to actions.loadContent along with pagination options, e.g. {schoolId: 123456} + contentQuery() { + throw new TypeError("contentQuery() has to be implemented by AdminSection subclasses."); } modalFormUI(record) { @@ -33,7 +50,7 @@ class AdminSection extends React.Component { ref="edit-modal" title={title} content={this.modalFormUI.bind(this)()} - submitCallback={this.options.submitCallback.bind(this)} + submitCallback={this.updateRecord.bind(this)} {...this.options} /> ); @@ -63,7 +80,9 @@ class AdminSection extends React.Component { key={index} className={action.class} onClick={action.action.bind(this, record)}> - + ); })} @@ -71,6 +90,77 @@ class AdminSection extends React.Component { ); } + onPageSizeChange(currentPage, itemsPerPage) { + this.setState({itemsPerPage}); + this.loadContent(1, itemsPerPage); + } + + loadContent(page, itemsPerPage) { + const paginationOptions = {$skip: (page - 1) * itemsPerPage, $limit: itemsPerPage}; + const query = Object.assign({}, paginationOptions, this.contentQuery()); + this.loadContentFromServer(query) + .then((result) => { + const numberOfPages = Math.ceil(result.pagination.total / this.state.itemsPerPage); + Object.assign(result, {numberOfPages}); + this.setState(result); + }); + } + + loadTeachers() { + this.props.actions.loadTeachers() + .then(teachers => this.setState({teachers})); + } + + loadClasses() { + this.props.actions.loadClasses() + .then(classes => this.setState({classes})); + } + + onPageChange(page) { + this.loadContent(page, this.state.itemsPerPage); + } + + updateRecord(data) { + console.info(`Replacing \n${JSON.stringify(this.state.records[data._id])} with \n${JSON.stringify(data)}`); + this.props.actions.updateRecord(this.serviceName, data) + .then(this.customizeRecordBeforeInserting.bind(this)) + .then(savedData => { + let records = this.state.records; + records[data._id] = savedData; + this.setState({records}); + }); + } + + // override point to customize records before they are inserted into the table, + // e.g. to populate fields (resolve ids) + customizeRecordBeforeInserting(data) { + return Promise.resolve(data); + } + + removeRecord(data) { + this.props.actions.removeRecord(this.serviceName, data) + .then(_ => { + let records = this.state.records; + delete records[data._id]; + this.setState({records}); + }); + } + + getPaginationControl() { + //if (this.state.numberOfPages < 2) return null; + return (); + } + render() { return (
@@ -80,13 +170,13 @@ class AdminSection extends React.Component {
{this.options.title}
+ {this.getPaginationControl()} - {this.modalUI()} ); diff --git a/src/modules/administration/components/administration.jsx b/src/modules/administration/components/administration.jsx index f5b5036085..2ad41eaf1f 100755 --- a/src/modules/administration/components/administration.jsx +++ b/src/modules/administration/components/administration.jsx @@ -2,11 +2,11 @@ import LayoutBase from '../../base/containers/layout'; import SectionTitle from '../../base/components/title'; /* only for base */ import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; -import SectionSchool from './school'; -import SectionCourses from './courses'; -import SectionClasses from './classes'; -import SectionTeachers from './teachers'; -import SectionStudents from './students'; +import SectionSchool from '../containers/school'; +import SectionCourses from '../containers/courses'; +import SectionClasses from '../containers/classes'; +import SectionTeachers from '../containers/teachers'; +import SectionStudents from '../containers/students'; require('../styles/administration.scss'); diff --git a/src/modules/administration/components/classes.jsx b/src/modules/administration/components/classes.jsx index 6dd23fc50d..23f8b4007f 100644 --- a/src/modules/administration/components/classes.jsx +++ b/src/modules/administration/components/classes.jsx @@ -10,14 +10,12 @@ class SectionClasses extends AdminSection { constructor(props) { super(props); - this.options = { + const options = { title: 'Klassen', addLabel: 'Klasse hinzufügen', - editLabel: 'Klasse bearbeiten', - submitCallback: (data) => { - this.props.actions.updateClass(data); - } + editLabel: 'Klasse bearbeiten' }; + Object.assign(this.options, options); this.actions = [ { @@ -25,10 +23,31 @@ class SectionClasses extends AdminSection { icon: 'edit' }, { - action: this.props.actions.removeClass.bind(this), + action: this.removeRecord, icon: 'trash-o' } - ] + ]; + + Object.assign(this.state, { + teachers: [], + classes: [] + }); + + this.loadContentFromServer = this.props.actions.loadContent.bind(this, '/classes'); + this.serviceName = '/classes'; + } + + componentDidMount() { + super.componentDidMount(); + this.loadTeachers(); + } + + contentQuery() { + const schoolId = this.props.schoolId; + return { + schoolId, + $populate: ['teacherIds'] + }; } getTableHead() { @@ -39,20 +58,25 @@ class SectionClasses extends AdminSection { ]; } + customizeRecordBeforeInserting(data) { + return this.props.actions.populateFields('/classes', data._id, ['teacherIds']); + } + getTableBody() { - return this.props.classes.map((c) => { + return Object.keys(this.state.records).map((id) => { + const c = this.state.records[id]; return [ c.name, - c.teacherIds.map(id => (this.props.teachersById[id] || {}).lastName).join(', '), + c.teacherIds.map(teacher => teacher.lastName).join(', '), this.getTableActions(this.actions, c) ]; }); } getTeacherOptions() { - return this.props.teachers.map((r) => { + return this.state.teachers.map((r) => { return { - label: r.lastName || r._id, + label: `${r.firstName || r._id} ${r.lastName || ""}`, value: r._id }; }); @@ -74,7 +98,7 @@ class SectionClasses extends AdminSection { name="schoolId" type="hidden" layout="elementOnly" - value={this.props.school._id} + value={this.props.schoolId} /> { - // TODO: make sure data.classId works on edit - this.props.actions.updateCourse(data); - } }; + Object.assign(this.options, options); this.actions = [ { @@ -26,10 +23,33 @@ class SectionCourses extends AdminSection { icon: 'edit' }, { - action: this.props.actions.removeCourse.bind(this), + action: this.removeRecord, icon: 'trash-o' } - ] + ]; + + Object.assign(this.state, {teachers: [], classes: []}); + + this.loadContentFromServer = this.props.actions.loadContent.bind(this, '/courses'); + this.serviceName = '/courses'; + } + + componentDidMount() { + super.componentDidMount(); + this.loadTeachers(); + this.loadClasses(); + } + + contentQuery() { + const schoolId = this.props.schoolId; + return { + schoolId, + $populate: ['classIds', 'teacherIds'] + }; + } + + customizeRecordBeforeInserting(data) { + return this.props.actions.populateFields('/courses', data._id, ['classIds', 'teacherIds']); } getTableHead() { @@ -42,27 +62,28 @@ class SectionCourses extends AdminSection { } getTableBody() { - return this.props.courses.map((c) => { + return Object.keys(this.state.records).map((id) => { + const c = this.state.records[id]; return [ c.name, - (c.classIds || []).map(id => this.props.classesById[id].name).join(', '), - c.teacherIds.map(id => (this.props.teachersById[id] || {}).lastName).join(', '), + (c.classIds || []).map(cl => cl.name).join(', '), + c.teacherIds.map(teacher => teacher.lastName).join(', '), this.getTableActions(this.actions, c) ]; }); } getTeacherOptions() { - return this.props.teachers.map((r) => { + return this.state.teachers.map((r) => { return { - label: r.lastName || r._id, + label: `${r.firstName || r._id} ${r.lastName || ""}`, value: r._id }; }); } getClassOptions() { - return this.props.classes.map((r) => { + return this.state.classes.map((r) => { return { label: r.name || r._id, value: r._id @@ -85,7 +106,7 @@ class SectionCourses extends AdminSection { name="schoolId" type="hidden" layout="elementOnly" - value={this.props.school._id} + value={this.props.schoolId} /> console.error(e)); } render() { diff --git a/src/modules/administration/components/students.jsx b/src/modules/administration/components/students.jsx index 3865b0f606..32bd703f53 100644 --- a/src/modules/administration/components/students.jsx +++ b/src/modules/administration/components/students.jsx @@ -9,21 +9,34 @@ class SectionStudents extends AdminSection { constructor(props) { super(props); - this.options = { + const options = { title: 'Schüler', addLabel: 'Schüler hinzufügen', editLabel: 'Schüler bearbeiten', - submitCallback: (data) => { - this.props.actions.updateUser(data); - } }; + Object.assign(this.options, options); this.actions = [ { action: this.openModal.bind(this), icon: 'edit' + }, + { + action: this.removeRecord, + icon: 'trash-o' } - ] + ]; + + this.loadContentFromServer = this.props.actions.loadContent.bind(this, '/users'); + this.serviceName = '/users'; + } + + contentQuery() { + const schoolId = this.props.schoolId; + return { + schoolId, + roles: ['student'] + }; } getTableHead() { @@ -36,7 +49,8 @@ class SectionStudents extends AdminSection { } getTableBody() { - return this.props.students.map((record) => { + return Object.keys(this.state.records).map((id) => { + const record = this.state.records[id]; return [ record.firstName, record.lastName, @@ -61,7 +75,7 @@ class SectionStudents extends AdminSection { name="schoolId" type="hidden" layout="elementOnly" - value={this.props.school._id} + value={this.props.schoolId} /> { - this.props.actions.updateUser(data); - } }; + Object.assign(this.options, options); this.actions = [ { action: this.openModal.bind(this), icon: 'edit' + }, + { + action: this.removeRecord, + icon: 'trash-o' } - ] + ]; + + this.loadContentFromServer = this.props.actions.loadContent.bind(this, '/users'); + this.serviceName = '/users'; + } + + contentQuery() { + const schoolId = this.props.schoolId; + return { + schoolId, + roles: ['teacher'], + $populate: ['roles'] + }; } getTableHead() { @@ -36,7 +50,8 @@ class SectionTeachers extends AdminSection { } getTableBody() { - return this.props.teachers.map((record) => { + return Object.keys(this.state.records).map((id) => { + const record = this.state.records[id]; return [ record.firstName, record.lastName, @@ -61,7 +76,7 @@ class SectionTeachers extends AdminSection { name="schoolId" type="hidden" layout="elementOnly" - value={this.props.school._id} + value={this.props.schoolId} /> { - const result = {}; - array.forEach((obj) => { - result[obj[key]] = obj; - }); - return result; -}; - -const composer = (props, onData) => { - - const currentUser = Server.get('user'); - - if(!Permissions.userHasPermission(currentUser, permissions.VIEW)) { - onData(new Error('You don\'t have the permission to see this page.')); - return; - } - - const schoolId = currentUser.schoolId; - - if(!schoolId) { - return onData(new Error("The current user is not associated with a school")); - } - - const subsManager = new SubsManager(); - - subsManager.addSubscription(schoolService.get(schoolId), 'school'); - - subsManager.addSubscription(courseService.find({query: {schoolId: schoolId}}), (courses) => { - return {courses: courses.data, coursesById: pluckArrayToObject(courses.data, '_id')}; - }); - - subsManager.addSubscription(classService.find({query: {schoolId: schoolId}}), (classes) => { - return {classes: classes.data, classesById: pluckArrayToObject(classes.data, '_id')}; - }); - - subsManager.addSubscription(userService.find({ - query: { - roles: ['teacher'], - $populate: ['roles'] - }, - rx: { - listStrategy: 'always', - idField: '_id', - matcher: query => item => { - // TODO: this should work out of the box - looks like a bug in the feathers-reactive module - return roleService.find({ - query: { - _id: { - $in: item.roles || [] - } - } - }).then((response) => { - return response.data.map(r => r.name).includes('teacher'); - }); - } - } - }), (teachers) => { - return {teachers: teachers.data, teachersById: pluckArrayToObject(teachers.data, '_id')}; - }); - - subsManager.addSubscription(userService.find({ - query: {roles: ['student']}, // TODO: no _id - rx: { - listStrategy: 'always', - idField: '_id', - matcher: query => item => { - // TODO: this should work out of the box - looks like a bug in the feathers-reactive module - return roleService.find({ - query: { - _id: { - $in: item.roles || [] - } - } - }).then((response) => { - return response.data.map(r => r.name).includes('student'); - }); - } - } - }), (students) => { - return {students: students.data, studentsById: pluckArrayToObject(students.data, '_id')}; - }); - - subsManager.ready((data, initial) => { - const componentData = Object.assign({}, {actions}, data); - onData(null, componentData); - }); - -}; - -export default compose(composer)(component); diff --git a/src/modules/administration/containers/classes.js b/src/modules/administration/containers/classes.js new file mode 100644 index 0000000000..04c8b99507 --- /dev/null +++ b/src/modules/administration/containers/classes.js @@ -0,0 +1,4 @@ +import SectionClasses from '../components/classes'; +import composer from './common'; + +export default composer(SectionClasses); diff --git a/src/modules/administration/containers/common.js b/src/modules/administration/containers/common.js new file mode 100644 index 0000000000..d9744f9f37 --- /dev/null +++ b/src/modules/administration/containers/common.js @@ -0,0 +1,38 @@ +import {render} from 'react-dom'; +import {compose} from 'react-komposer'; +import { Permissions, Server } from '../../core/helpers/'; + +import { SubsManager } from 'feathers-subscriptions-manager'; + +import permissions from '../permissions'; + +import actions from '../actions/administration'; + +const composer = (props, onData) => { + + const currentUser = Server.get('user'); + + if(!Permissions.userHasPermission(currentUser, permissions.VIEW)) { + onData(new Error('You don\'t have the permission to see this page.')); + return; + } + + const schoolId = currentUser.schoolId; + + if(!schoolId) { + return onData(new Error("The current user is not associated with a school")); + } + + // bind the schoolId parameter for cleaner calls in the components + actions.loadTeachers = actions._loadTeachers.bind(null, schoolId); + actions.loadClasses = actions._loadClasses.bind(null, schoolId); + + Server.service('/schools').get(schoolId) + .then(school => { + const componentData = {actions, schoolId, school}; + onData(null, componentData); + }) + .catch(error => onData(error)); +}; + +export default compose(composer); diff --git a/src/modules/administration/containers/courses.js b/src/modules/administration/containers/courses.js new file mode 100644 index 0000000000..81ffdd5750 --- /dev/null +++ b/src/modules/administration/containers/courses.js @@ -0,0 +1,4 @@ +import SectionCourses from '../components/courses'; +import composer from './common'; + +export default composer(SectionCourses); diff --git a/src/modules/administration/containers/school.js b/src/modules/administration/containers/school.js new file mode 100644 index 0000000000..d6ca7537ec --- /dev/null +++ b/src/modules/administration/containers/school.js @@ -0,0 +1,5 @@ + +import SectionSchool from '../components/school'; +import composer from './common'; + +export default composer(SectionSchool); diff --git a/src/modules/administration/containers/students.js b/src/modules/administration/containers/students.js new file mode 100644 index 0000000000..9740ecbf89 --- /dev/null +++ b/src/modules/administration/containers/students.js @@ -0,0 +1,4 @@ +import SectionStudents from '../components/students'; +import composer from './common'; + +export default composer(SectionStudents); diff --git a/src/modules/administration/containers/teachers.js b/src/modules/administration/containers/teachers.js new file mode 100644 index 0000000000..5344deb256 --- /dev/null +++ b/src/modules/administration/containers/teachers.js @@ -0,0 +1,4 @@ +import SectionTeachers from '../components/teachers'; +import composer from './common'; + +export default composer(SectionTeachers); diff --git a/src/modules/administration/routes.jsx b/src/modules/administration/routes.jsx index 978ad0a6ad..d2b05f67b5 100644 --- a/src/modules/administration/routes.jsx +++ b/src/modules/administration/routes.jsx @@ -1,4 +1,4 @@ -import Administration from './containers/administration'; +import Administration from './components/administration'; export default [ { diff --git a/src/modules/administration/styles/rc-pagination.scss b/src/modules/administration/styles/rc-pagination.scss new file mode 100644 index 0000000000..1eb1500d8c --- /dev/null +++ b/src/modules/administration/styles/rc-pagination.scss @@ -0,0 +1,184 @@ +@import '../../core/styles/colors.scss'; + +.rc-pagination { + font-size: 12px; + font-family: 'Arial'; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-align: center; +} +.rc-pagination-total-text { + height: 30px; + line-height: 30px; + margin-right: 10px; +} +.rc-pagination:after { + content: " "; + display: block; + height: 0; + clear: both; + overflow: hidden; + visibility: hidden; +} +.rc-pagination-item { + cursor: pointer; + border-radius: 6px; + display: inline-block; + min-width: 28px; + height: 28px; + line-height: 28px; + text-align: center; + list-style: none; + border: 1px solid #d9d9d9; + background-color: #fff; + margin-right: 8px; +} +.rc-pagination-item a { + text-decoration: none; + color: #666; +} +.rc-pagination-item:hover { + border-color: $colorHPIRed; +} +.rc-pagination-item:hover a { + color: $colorHPIRed; +} +.rc-pagination-item-active { + border-color: $colorHPIRed; + background-color: $colorHPIRed; + color: #fff; +} +.rc-pagination-item-active a { + color: #fff; +} +.rc-pagination-item-active:hover a { + color: #fff; +} +.rc-pagination-jump-prev:after, +.rc-pagination-jump-next:after { + content: "•••"; + display: block; + letter-spacing: 2px; + color: #ccc; + font-size: 12px; + margin-top: 1px; +} +.rc-pagination-jump-prev:hover:after, +.rc-pagination-jump-next:hover:after { + color: $colorHPIRed; +} +.rc-pagination-jump-prev:hover:after { + content: "«"; +} +.rc-pagination-jump-next:hover:after { + content: "»"; +} +.rc-pagination-prev, +.rc-pagination-jump-prev, +.rc-pagination-jump-next { + margin-right: 8px; +} +.rc-pagination-prev, +.rc-pagination-next, +.rc-pagination-jump-prev, +.rc-pagination-jump-next { + cursor: pointer; + color: #666; + display: inline-block; + font-size: 10px; + border-radius: 6px; + list-style: none; + min-width: 28px; + height: 28px; + line-height: 28px; + text-align: center; +} +.rc-pagination-prev a:after { + content: "‹"; + display: block; +} +.rc-pagination-next a:after { + content: "›"; + display: block; +} +.rc-pagination-prev, +.rc-pagination-next { + border: 1px solid #d9d9d9; + font-size: 15px; +} +.rc-pagination-prev a, +.rc-pagination-next a { + color: #666; +} +.rc-pagination-prev a:after, +.rc-pagination-next a:after { + margin-top: -1px; +} +.rc-pagination-disabled { + cursor: not-allowed; +} +.rc-pagination-disabled a { + color: #ccc; +} +.rc-pagination-options { + margin-left: 15px; + display: inline-block; +} +.rc-pagination-options-size-changer { + width: 80px; +} +.rc-pagination-options-quick-jumper { + margin-left: 16px; + height: 28px; + line-height: 28px; +} +.rc-pagination-options-quick-jumper input { + margin: 0 8px; + box-sizing: border-box; + background-color: #fff; + border-radius: 6px; + border: 1px solid #d9d9d9; + outline: none; + padding: 3px 12px; + width: 50px; + height: 28px; +} +.rc-pagination-options-quick-jumper input:hover { + border-color: $colorHPIRed; +} +.rc-pagination-simple .rc-pagination-prev, +.rc-pagination-simple .rc-pagination-next { + border: none; + height: 24px; + line-height: 24px; + margin: 0; + font-size: 18px; +} +.rc-pagination-simple .rc-pagination-simple-pager { + margin-right: 8px; +} +.rc-pagination-simple .rc-pagination-simple-pager .rc-pagination-slash { + margin: 0 10px; +} +.rc-pagination-simple .rc-pagination-simple-pager input { + margin: 0 8px; + box-sizing: border-box; + background-color: #fff; + border-radius: 6px; + border: 1px solid #d9d9d9; + outline: none; + padding: 5px 8px; + width: 30px; + min-height: 20px; +} +.rc-pagination-simple .rc-pagination-simple-pager input:hover { + border-color: $colorHPIRed; +} +@media only screen and (max-width: 1024px) { + .rc-pagination-item-after-jump-prev, + .rc-pagination-item-before-jump-next { + display: none; + } +} diff --git a/src/modules/administration/styles/rc-select.scss b/src/modules/administration/styles/rc-select.scss new file mode 100644 index 0000000000..ad3dc3e751 --- /dev/null +++ b/src/modules/administration/styles/rc-select.scss @@ -0,0 +1,565 @@ +@import '../../core/styles/colors.scss'; + +.rc-select { + box-sizing: border-box; + display: inline-block; + position: relative; + vertical-align: top; + color: #666; + line-height: 28px; +} +.rc-select-allow-clear .rc-select-selection--single .rc-select-selection__rendered { + padding-right: 40px; +} +.rc-select ul, +.rc-select li { + margin: 0; + padding: 0; + list-style: none; +} +.rc-select > ul > li > a { + padding: 0; + background-color: #fff; +} +.rc-select-arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + outline: none; +} +.rc-select-arrow b { + border-color: #999999 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + width: 0; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + left: 50%; +} +.rc-select-selection { + outline: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + box-sizing: border-box; + display: block; + background-color: #fff; + border-radius: 6px; + border: 1px solid #d9d9d9; +} +.rc-select-selection__placeholder { + position: absolute; + top: 0; + color: #aaa; +} +.rc-select-focused .rc-select-selection { + border-color: $colorHPIRed; + background-color: $colorHPIRed; + color: #fff; +} +.rc-select-enabled .rc-select-selection:hover { + background-color: #7f0328; +} +.rc-select-enabled .rc-select-selection:active { + background-color: #7f0328; +} +.rc-select-selection--single { + height: 28px; + line-height: 28px; + cursor: pointer; + position: relative; +} +.rc-select-selection--single .rc-select-selection-selected-value { + position: absolute; + left: 0; + top: 0; +} +.rc-select-selection--single .rc-select-selection__rendered { + height: 28px; + position: relative; + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-left: 10px; + padding-right: 20px; + line-height: 28px; +} +.rc-select-selection--single .rc-select-selection__clear { + font-weight: bold; + position: absolute; + top: 0; + right: 20px; + line-height: 28px; +} +.rc-select-selection--single .rc-select-selection__clear:after { + content: '×'; +} +.rc-select-disabled { + color: #ccc; + cursor: not-allowed; +} +.rc-select-disabled .rc-select-selection--single, +.rc-select-disabled .rc-select-selection__choice__remove { + cursor: not-allowed; + color: #ccc; +} +.rc-select-disabled .rc-select-selection--single:hover, +.rc-select-disabled .rc-select-selection__choice__remove:hover { + cursor: not-allowed; + color: #ccc; +} +.rc-select-search__field__wrap { + display: inline-block; +} +.rc-select-search__field__placeholder { + position: absolute; + top: 0; + left: 3px; + color: #aaa; +} +.rc-select-search--inline { + width: 100%; +} +.rc-select-search--inline .rc-select-search__field__wrap { + width: 100%; +} +.rc-select-search--inline .rc-select-search__field { + border: none; + font-size: 100%; + background: transparent; + outline: 0; + width: 100%; +} +.rc-select-search--inline .rc-select-search__field::-ms-clear { + display: none; +} +.rc-select-search--inline .rc-select-search__field__mirror { + position: absolute; + top: -999px; + left: 0; + white-space: pre; +} +.rc-select-search--inline > i { + float: right; +} +.rc-select-enabled.rc-select-selection--multiple { + cursor: text; +} +.rc-select-selection--multiple { + min-height: 28px; +} +.rc-select-selection--multiple .rc-select-search--inline { + float: left; + width: auto; +} +.rc-select-selection--multiple .rc-select-search--inline .rc-select-search__field { + width: 0.75em; +} +.rc-select-selection--multiple .rc-select-search--inline .rc-select-search__field__wrap { + width: auto; +} +.rc-select-selection--multiple .rc-select-search__field__placeholder { + top: 5px; + left: 8px; +} +.rc-select-selection--multiple .rc-select-selection__rendered { + position: relative; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 8px; + padding-bottom: 2px; +} +.rc-select-selection--multiple .rc-select-selection__rendered .rc-select-selection__choice { + margin-top: 4px; + line-height: 20px; +} +.rc-select-enabled .rc-select-selection__choice { + cursor: default; +} +.rc-select-enabled .rc-select-selection__choice:hover .rc-select-selection__choice__remove { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); +} +.rc-select-enabled .rc-select-selection__choice:hover .rc-select-selection__choice__content { + margin-left: -8px; + margin-right: 8px; +} +.rc-select-enabled .rc-select-selection__choice__disabled { + cursor: not-allowed; +} +.rc-select-enabled .rc-select-selection__choice__disabled:hover .rc-select-selection__choice__content { + margin-left: 0; + margin-right: 0; +} +.rc-select .rc-select-selection__choice { + background-color: #f3f3f3; + border-radius: 4px; + float: left; + padding: 0 15px; + margin-right: 4px; + position: relative; + overflow: hidden; + -webkit-transition: padding 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045), width 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045); + transition: padding 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045), width 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045); +} +.rc-select .rc-select-selection__choice__content { + margin-left: 0; + margin-right: 0; + -webkit-transition: margin 0.3s cubic-bezier(0.165, 0.84, 0.44, 1); + transition: margin 0.3s cubic-bezier(0.165, 0.84, 0.44, 1); +} +.rc-select .rc-select-selection__choice-zoom-enter, +.rc-select .rc-select-selection__choice-zoom-appear, +.rc-select .rc-select-selection__choice-zoom-leave { + -webkit-animation-duration: .3s; + animation-duration: .3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + opacity: 0; + -webkit-animation-play-state: paused; + animation-play-state: paused; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); +} +.rc-select .rc-select-selection__choice-zoom-leave { + opacity: 1; + -webkit-animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); + animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); +} +.rc-select .rc-select-selection__choice-zoom-enter.rc-select-selection__choice-zoom-enter-active, +.rc-select .rc-select-selection__choice-zoom-appear.rc-select-selection__choice-zoom-appear-active { + -webkit-animation-play-state: running; + animation-play-state: running; + -webkit-animation-name: rcSelectChoiceZoomIn; + animation-name: rcSelectChoiceZoomIn; +} +.rc-select .rc-select-selection__choice-zoom-leave.rc-select-selection__choice-zoom-leave-active { + -webkit-animation-play-state: running; + animation-play-state: running; + -webkit-animation-name: rcSelectChoiceZoomOut; + animation-name: rcSelectChoiceZoomOut; +} +@-webkit-keyframes rcSelectChoiceZoomIn { + 0% { + -webkit-transform: scale(0.6); + transform: scale(0.6); + opacity: 0; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; + } +} +@keyframes rcSelectChoiceZoomIn { + 0% { + -webkit-transform: scale(0.6); + transform: scale(0.6); + opacity: 0; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; + } +} +@-webkit-keyframes rcSelectChoiceZoomOut { + to { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0; + } +} +@keyframes rcSelectChoiceZoomOut { + to { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0; + } +} +.rc-select .rc-select-selection__choice__remove { + color: #919191; + cursor: pointer; + font-weight: bold; + padding: 0 0 0 8px; + position: absolute; + opacity: 0; + -webkit-transform: scale(0); + transform: scale(0); + top: 0; + right: 2px; + -webkit-transition: opacity .3s, -webkit-transform .3s; + transition: opacity .3s, -webkit-transform .3s; + transition: opacity .3s, transform .3s; + transition: opacity .3s, transform .3s, -webkit-transform .3s; +} +.rc-select .rc-select-selection__choice__remove:before { + content: '×'; +} +.rc-select .rc-select-selection__choice__remove:hover { + color: #333; +} +.rc-select-dropdown { + background-color: white; + border: 1px solid #d9d9d9; + box-shadow: 0 0px 4px #d9d9d9; + border-radius: 4px; + box-sizing: border-box; + z-index: 100; + left: -9999px; + top: -9999px; + position: absolute; + outline: none; +} +.rc-select-dropdown:empty, +.rc-select-dropdown-hidden { + display: none; +} +.rc-select-dropdown-menu { + outline: none; + margin: 0; + padding: 0; + list-style: none; + z-index: 9999; +} +.rc-select-dropdown-menu > li { + margin: 0; + padding: 0; +} +.rc-select-dropdown-menu-item-group-list { + margin: 0; + padding: 0; +} +.rc-select-dropdown-menu-item-group-list > li.rc-select-menu-item { + padding-left: 20px; +} +.rc-select-dropdown-menu-item-group-title { + color: #999; + line-height: 1.5; + padding: 8px 10px; + border-bottom: 1px solid #dedede; +} +li.rc-select-dropdown-menu-item { + margin: 0; + position: relative; + display: block; + padding: 7px 10px; + font-weight: normal; + color: #666; + white-space: nowrap; +} +li.rc-select-dropdown-menu-item-disabled { + color: #ccc; + cursor: not-allowed; +} +li.rc-select-dropdown-menu-item-selected { + color: #666; + background-color: #ddd; +} +li.rc-select-dropdown-menu-item-active { + background-color: #5897fb; + color: white; + cursor: pointer; +} +li.rc-select-dropdown-menu-item-divider { + height: 1px; + margin: 1px 0; + overflow: hidden; + background-color: #e5e5e5; + line-height: 0; +} +.rc-select-dropdown-slide-up-enter, +.rc-select-dropdown-slide-up-appear { + -webkit-animation-duration: .3s; + animation-duration: .3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + opacity: 0; + -webkit-animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + -webkit-animation-play-state: paused; + animation-play-state: paused; +} +.rc-select-dropdown-slide-up-leave { + -webkit-animation-duration: .3s; + animation-duration: .3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + opacity: 1; + -webkit-animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); + animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); + -webkit-animation-play-state: paused; + animation-play-state: paused; +} +.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-bottomLeft, +.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-bottomLeft { + -webkit-animation-name: rcSelectDropdownSlideUpIn; + animation-name: rcSelectDropdownSlideUpIn; + -webkit-animation-play-state: running; + animation-play-state: running; +} +.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-bottomLeft { + -webkit-animation-name: rcSelectDropdownSlideUpOut; + animation-name: rcSelectDropdownSlideUpOut; + -webkit-animation-play-state: running; + animation-play-state: running; +} +.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-topLeft, +.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-topLeft { + -webkit-animation-name: rcSelectDropdownSlideDownIn; + animation-name: rcSelectDropdownSlideDownIn; + -webkit-animation-play-state: running; + animation-play-state: running; +} +.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-topLeft { + -webkit-animation-name: rcSelectDropdownSlideDownOut; + animation-name: rcSelectDropdownSlideDownOut; + -webkit-animation-play-state: running; + animation-play-state: running; +} +@-webkit-keyframes rcSelectDropdownSlideUpIn { + 0% { + opacity: 0; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } + 100% { + opacity: 1; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } +} +@keyframes rcSelectDropdownSlideUpIn { + 0% { + opacity: 0; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } + 100% { + opacity: 1; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } +} +@-webkit-keyframes rcSelectDropdownSlideUpOut { + 0% { + opacity: 1; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } + 100% { + opacity: 0; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } +} +@keyframes rcSelectDropdownSlideUpOut { + 0% { + opacity: 1; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } + 100% { + opacity: 0; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } +} +@-webkit-keyframes rcSelectDropdownSlideDownIn { + 0% { + opacity: 0; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } + 100% { + opacity: 1; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } +} +@keyframes rcSelectDropdownSlideDownIn { + 0% { + opacity: 0; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } + 100% { + opacity: 1; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } +} +@-webkit-keyframes rcSelectDropdownSlideDownOut { + 0% { + opacity: 1; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } + 100% { + opacity: 0; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } +} +@keyframes rcSelectDropdownSlideDownOut { + 0% { + opacity: 1; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(1); + transform: scaleY(1); + } + 100% { + opacity: 0; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + -webkit-transform: scaleY(0); + transform: scaleY(0); + } +} +.rc-select-open .rc-select-arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; +} diff --git a/src/modules/signup/actions/signup.js b/src/modules/signup/actions/signup.js index 532b93a549..4b87d075c0 100644 --- a/src/modules/signup/actions/signup.js +++ b/src/modules/signup/actions/signup.js @@ -4,6 +4,7 @@ import { Server, Notification } from '../../core/helpers'; const accountService = Server.service('/accounts'); const userService = Server.service('/users'); +const schoolService = Server.service('/schools'); const afterSignupUserRegular = (user, data) => { data.userId = user._id; @@ -54,6 +55,11 @@ export default { }); }, + updateSchool(data) { + if (data._id) return schoolService.patch(data._id, data); + return schoolService.create(data); + }, + finishSetup(userId) { return userService.patch(userId, { system: { diff --git a/src/modules/signup/components/setup-form-classes.jsx b/src/modules/signup/components/setup-form-classes.jsx index 121fcd6ee2..1ec5cd452f 100644 --- a/src/modules/signup/components/setup-form-classes.jsx +++ b/src/modules/signup/components/setup-form-classes.jsx @@ -14,7 +14,7 @@ import { import {Link} from 'react-router'; import Table from '../../administration/components/table'; -import AdminSectionClasses from '../../administration/components/classes'; +import AdminSectionClasses from '../../administration/containers/classes'; class SetupFormClasses extends AdminSectionClasses { @@ -31,25 +31,13 @@ class SetupFormClasses extends AdminSectionClasses {

Tipp: Lehrer können auch selber Klassen anlegen.

- -
-
-
- - - - - - - {this.modalUI()} +

Im nächsten Schritt können Sie Kurse anlegen:

- Fortsetzen + Fortsetzen ); diff --git a/src/modules/signup/components/setup-form-courses.jsx b/src/modules/signup/components/setup-form-courses.jsx index 84d9e499fc..e92154e8a9 100644 --- a/src/modules/signup/components/setup-form-courses.jsx +++ b/src/modules/signup/components/setup-form-courses.jsx @@ -14,7 +14,7 @@ import { import {Link} from 'react-router'; import Table from '../../administration/components/table'; -import AdminSectionCourses from '../../administration/components/courses'; +import AdminSectionCourses from '../../administration/containers/courses'; class SetupFormCourses extends AdminSectionCourses { @@ -33,19 +33,7 @@ class SetupFormCourses extends AdminSectionCourses {

Tipp: Lehrer können auch selber Kurse anlegen.

-
-
-
-
- - - - - - - {this.modalUI()} +
diff --git a/src/modules/signup/components/setup-form-teachers.jsx b/src/modules/signup/components/setup-form-teachers.jsx index a62b7aba27..a3b7ffcd10 100644 --- a/src/modules/signup/components/setup-form-teachers.jsx +++ b/src/modules/signup/components/setup-form-teachers.jsx @@ -14,7 +14,7 @@ import { import {Link} from 'react-router'; import Table from '../../administration/components/table'; -import AdminSectionTeachers from '../../administration/components/teachers'; +import AdminSectionTeachers from '../../administration/containers/teachers'; class SetupFormTeachers extends AdminSectionTeachers { @@ -33,25 +33,13 @@ class SetupFormTeachers extends AdminSectionTeachers {

Tipp: Sie können auch nach Abschluss der Registrierung jeder Zeit weitere Lehrkräfte hinzufügen.

-
-
-
-
- - - - - - - {this.modalUI()} +

Im nächsten Schritt können Sie Klassen anlegen.

- Fortsetzen + Fortsetzen ); } diff --git a/src/modules/signup/components/setup.jsx b/src/modules/signup/components/setup.jsx index f4b5a0345a..5436e122bd 100755 --- a/src/modules/signup/components/setup.jsx +++ b/src/modules/signup/components/setup.jsx @@ -6,7 +6,7 @@ import SetupFormCourses from './setup-form-courses'; require('../styles/signup.scss'); -class Signup extends React.Component { +class Setup extends React.Component { constructor(props) { super(props); @@ -49,4 +49,4 @@ class Signup extends React.Component { } -export default Signup; +export default Setup; diff --git a/src/modules/signup/containers/setup.js b/src/modules/signup/containers/setup.js index 096e1711e5..8b6798baf9 100644 --- a/src/modules/signup/containers/setup.js +++ b/src/modules/signup/containers/setup.js @@ -8,11 +8,6 @@ import adminActions from '../../administration/actions/administration'; import component from '../components/setup'; import actions from '../actions/signup'; -const schoolService = Server.service('/schools'); -const courseService = Server.service('/courses'); -const classService = Server.service('/classes'); -const userService = Server.service('/users'); -const roleService = Server.service('/roles'); import { Server } from '../../core/helpers'; @@ -46,63 +41,30 @@ function composer(props, onData) { return; } - const subsManager = new SubsManager(); + const schoolId = currentUser.schoolId; - const schoolId = '584ad186816abba584714c94'; - - subsManager.addSubscription(schoolService.get(schoolId), 'school'); - - subsManager.addSubscription(courseService.find({query: {schoolId: schoolId}}), (courses) => { - return {courses: courses.data, coursesById: pluckArrayToObject(courses.data, '_id')}; - }); - - subsManager.addSubscription(classService.find({query: {schoolId: schoolId}}), (classes) => { - return {classes: classes.data, classesById: pluckArrayToObject(classes.data, '_id')}; - }); - - subsManager.addSubscription(userService.find({ - query: { - roles: ['teacher'], - $populate: ['roles'] + const componentData = { + actions: combinedActions, + step, + onUpdateSchool: (data) => { + actions.updateSchool(data).then(() => { + browserHistory.push("/setup/teachers/"); + }); }, - rx: { - listStrategy: 'always', - idField: '_id', - matcher: query => item => { - // TODO: this should work out of the box - looks like a bug in the feathers-reactive module - return roleService.find({ - query: { - _id: { - $in: item.roles || [] - } - } - }).then((response) => { - return response.data.map(r => r.name).includes('teacher'); - }); - } + onSignupFinished: () => { + actions.finishSetup(currentUser._id).then(() => { + browserHistory.push("/administration/"); + }); } - }), (teachers) => { - return {teachers: teachers.data, teachersById: pluckArrayToObject(teachers.data, '_id')}; - }); + }; + + Server.service('/schools').get(schoolId) + .then(school => { + Object.assign(componentData, {schoolId, school}); + onData(null, componentData); + }) + .catch(error => onData(error)); - subsManager.ready((data, initial) => { - const componentData = Object.assign({ - actions: combinedActions, - step, - onUpdateSchool: (data) => { - adminActions.updateSchool(data).then(() => { - browserHistory.push("/signup/teachers/"); - }); - }, - onSignupFinished: () => { - actions.finishSignup(currentUser._id).then(() => { - browserHistory.push("/administration/"); - }); - } - }, data); - - onData(null, componentData); - }); } export default compose(composer)(component);