diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..a7554c2
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+# dependencies
+/node_modules
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e1140ce
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+
+# testing
+/coverage
+
+# production
+/build
+/docker/files/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+test-results.json
+
+
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..31be39d
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,32 @@
+.App {
+ text-align: center;
+}
+
+.App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ height: 80px;
+}
+
+.App-header {
+ background-color: #222;
+ height: 150px;
+ padding: 20px;
+ color: white;
+}
+
+.App-title {
+ font-size: 1.5em;
+}
+
+.App-intro {
+ font-size: large;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 0000000..899d872
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,104 @@
+import React, { Component } from "react";
+import { Provider } from "react-redux";
+import { PersistGate } from "redux-persist/integration/react";
+import { I18nextProvider } from "react-i18next";
+
+/**
+ * Look here for more information on ConnectedRouter
+ * (can be easily confused with the old react-router-redux)...
+ * https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux
+ */
+import { ConnectedRouter } from "react-router-redux";
+import { Route } from "react-router";
+
+import { changeLanguage, i18n } from "./i18n";
+import configureStore from "./redux/createStore";
+
+import { MuiThemeProvider, createMuiTheme } from "material-ui/styles";
+
+import Login from "./components/Login";
+import UserList from "./components/UserList";
+import UserCreateScreen from "./components/UserCreateScreen";
+import RoleCreateScreen from "./components/RoleCreateScreen";
+import LandingPage from "./components/LandingPage";
+import GroupCreateScreen from "./components/GroupCreateScreen";
+import RoleList from "./components/RoleList";
+import GroupList from "./components/GroupList";
+import ContactList from "./components/ContactList";
+
+const { store, history, persistor } = configureStore();
+const theme = createMuiTheme({
+ custom: {
+ drawer: {
+ width: 240,
+ closedWidth: 90
+ }
+ }
+});
+
+class App extends Component {
+ constructor(props) {
+ super(props);
+ //Use a fixed language in development for now
+ if (process.env.NODE_ENV === "development") {
+ changeLanguage("en");
+ //console.log(i18n);
+ }
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default App;
diff --git a/src/App.test.js b/src/App.test.js
new file mode 100644
index 0000000..989fb85
--- /dev/null
+++ b/src/App.test.js
@@ -0,0 +1,8 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+import { shallow } from "enzyme";
+
+it.skip("renders without crashing", () => {
+ shallow();
+});
diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js
new file mode 100644
index 0000000..8bc323c
--- /dev/null
+++ b/src/components/Checkbox.js
@@ -0,0 +1,60 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "material-ui/styles";
+import green from "material-ui/colors/green";
+import { FormGroup, FormControlLabel } from "material-ui/Form";
+import Checkbox from "material-ui/Checkbox";
+
+const styles = {
+ root: {
+ color: green[600],
+ "&$checked": {
+ color: green[500]
+ }
+ },
+ checked: {},
+ size: {
+ width: 40,
+ height: 40
+ },
+ sizeIcon: {
+ fontSize: 20
+ }
+};
+
+/**
+ * state = {
+ checkedA: true,
+ };
+
+ handleChange = name => event => {
+ this.setState({ [name]: event.target.checked });
+ };
+ */
+class CheckboxLabels extends React.Component {
+ render() {
+ const { label, checked, name, onChange } = this.props;
+
+ return (
+
+ onChange(name, event.target.checked)}
+ value="checked"
+ color="primary"
+ />
+ }
+ label={label}
+ />
+
+ );
+ }
+}
+
+CheckboxLabels.propTypes = {
+ classes: PropTypes.object.isRequired
+};
+
+export default withStyles(styles)(CheckboxLabels);
diff --git a/src/components/ContactList.js b/src/components/ContactList.js
new file mode 100644
index 0000000..a912e7a
--- /dev/null
+++ b/src/components/ContactList.js
@@ -0,0 +1,207 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "material-ui/styles";
+import Table, {
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+ TableFooter,
+ TablePagination
+} from "material-ui/Table";
+import { connect } from "react-redux";
+import { fetchList } from "../redux/actions";
+import { getItems, getItemById } from "../redux/selectors";
+import IconButton from "material-ui/IconButton";
+import KeyboardArrowLeft from "material-ui-icons/KeyboardArrowLeft";
+import KeyboardArrowRight from "material-ui-icons/KeyboardArrowRight";
+import Screen from "./Screen";
+import { entity } from "../lib/entity";
+import { apiMethod } from "../config";
+
+const actionsStyles = theme => ({
+ root: {
+ flexShrink: 0,
+ color: theme.palette.text.secondary,
+ marginLeft: theme.spacing.unit * 2.5
+ }
+});
+
+class TablePaginationActions extends React.Component {
+ handleBackButtonClick = event => {
+ this.props.onChangePage(event, this.props.page - 1);
+ };
+
+ handleNextButtonClick = event => {
+ this.props.onChangePage(event, this.props.page + 1);
+ };
+
+ render() {
+ const { classes, count, page, rowsPerPage, theme } = this.props;
+
+ return (
+
+
+ {theme.direction === "rtl" ? (
+
+ ) : (
+
+ )}
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="Next Page"
+ >
+ {theme.direction === "rtl" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ }
+}
+
+TablePaginationActions.propTypes = {
+ classes: PropTypes.object.isRequired,
+ count: PropTypes.number.isRequired,
+ onChangePage: PropTypes.func.isRequired,
+ page: PropTypes.number.isRequired,
+ rowsPerPage: PropTypes.number.isRequired,
+ theme: PropTypes.object.isRequired
+};
+
+const TablePaginationActionsWrapped = withStyles(actionsStyles, {
+ withTheme: true
+})(TablePaginationActions);
+
+const styles = theme => ({
+ root: {
+ width: "100%",
+ marginTop: theme.spacing.unit * 3,
+ overflowX: "auto"
+ },
+ table: {
+ minWidth: "20%",
+ maxWidth: "90%"
+ },
+ tableWrapper: {
+ overflowX: "auto"
+ }
+});
+
+class ContactList extends Component {
+ componentWillMount = () => {
+ const { fetchContacts } = this.props;
+ fetchContacts();
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ page: 0,
+ rowsPerPage: 5
+ };
+ }
+
+ handleChangePage = (event, page) => {
+ this.setState({ page });
+ };
+
+ handleChangeRowsPerPage = event => {
+ this.setState({ rowsPerPage: event.target.value });
+ };
+
+ render() {
+ const { classes, users } = this.props;
+ const { rowsPerPage, page } = this.state;
+
+ //console.log("Render a component", users, this.props.user(6));
+ return (
+
+ Übersicht aller Mitglieder
+ {users ? (
+
+
+
+
+ Mitgliedsnummer
+ Name
+ Email
+ Rollen
+
+
+
+ {users
+ .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+ .map(user => {
+ return (
+
+ {user.id}
+
+ {user.firstName} {user.lastName}
+
+ {user.email}
+
+ {user.roles ? (
+
+ {user.roles.map(role => {
+ return
{role.name}
;
+ })}
+
+ ) : null}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ ) : (
+ Es existieren keine Mitglieder!
+ )}
+
+ );
+ }
+}
+
+ContactList.propTypes = {
+ classes: PropTypes.object.isRequired
+};
+
+const mapStateToProps = state => {
+ return {
+ users: getItems(apiMethod.list)(entity.contact)(state),
+ user: id => getItemById(apiMethod.list)(entity.contact)(id)(state)
+ };
+};
+
+const mapDispatchToProps = {
+ fetchContacts: fetchList(entity.contact)
+};
+
+export const ConnectedContactList = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ContactList);
+
+export default withStyles(styles)(ConnectedContactList);
diff --git a/src/components/GroupCreateForm.js b/src/components/GroupCreateForm.js
new file mode 100644
index 0000000..40816b0
--- /dev/null
+++ b/src/components/GroupCreateForm.js
@@ -0,0 +1,183 @@
+import { Field, reduxForm } from "redux-form";
+import { withStyles } from "material-ui/styles/index";
+import { Link } from "react-router-dom";
+import React from "react";
+import PropTypes from "prop-types";
+import Typography from "material-ui/Typography";
+import Paper from "material-ui/Paper";
+import { TextField, Button } from "material-ui";
+import "react-select/dist/react-select.css";
+import ReactSelect from "./ReactSelect";
+import { translate } from "react-i18next";
+
+const styles = theme => ({
+ chip: {
+ margin: theme.spacing.unit / 4
+ },
+ root: theme.mixins.gutters({
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ paddingTop: 16,
+ paddingBottom: 16,
+ margin: `${theme.spacing.unit * 3}px auto 0`,
+ width: 700
+ }),
+ formColumn: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "flex-start"
+ },
+ formRow: {
+ display: "flex",
+ flexDirection: "row",
+ width: "100%"
+ },
+ formItem: {
+ margin: theme.spacing.unit,
+ width: "100%"
+ },
+ buttonsRow: {
+ display: "flex",
+ flexDirection: "row",
+ alignItems: "center",
+ margin: 25
+ },
+ buttonRow: {
+ display: "flex",
+ flexDirection: "row",
+ margin: 5
+ },
+ cancelButton: {
+ "text-decoration": "none"
+ }
+});
+
+const renderTextField = ({ input, meta: { touched, error }, ...rest }) => (
+
+);
+
+export class GroupCreateForm extends React.Component {
+ render() {
+ const {
+ classes,
+ handleDelete,
+ handleSubmit,
+ canSubmit,
+ users,
+ contacts,
+ multiUsers,
+ multiContacts,
+ multiResponsibles,
+ singlePermission,
+ permissionToChoose,
+ handleChange,
+ handleChangeSingle,
+ id,
+ t
+ } = this.props;
+ var validID = !!id || id === 0;
+ return (
+
+ );
+ }
+}
+
+GroupCreateForm.propTypes = {
+ classes: PropTypes.object.isRequired,
+ t: PropTypes.func.isRequired,
+ handleChangeSingle: PropTypes.func.isRequired,
+ handleChange: PropTypes.func.isRequired
+};
+
+export default reduxForm({ form: "GroupCreateForm", enableReinitialize: true })(
+ withStyles(styles)(translate()(GroupCreateForm))
+);
diff --git a/src/components/GroupCreateForm.test.js b/src/components/GroupCreateForm.test.js
new file mode 100644
index 0000000..866aecd
--- /dev/null
+++ b/src/components/GroupCreateForm.test.js
@@ -0,0 +1,21 @@
+import React from "react";
+import { shallow } from "enzyme";
+import { GroupCreateForm } from "./GroupCreateForm";
+
+it("renders without crashing", () => {
+ shallow(
+
+ );
+});
diff --git a/src/components/GroupCreateScreen.js b/src/components/GroupCreateScreen.js
new file mode 100644
index 0000000..377db73
--- /dev/null
+++ b/src/components/GroupCreateScreen.js
@@ -0,0 +1,312 @@
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import { translate } from "react-i18next";
+import { CircularProgress, withStyles, Snackbar } from "material-ui";
+import { permission } from "../config/groupPermissionTypes";
+
+import GroupCreateForm from "./GroupCreateForm";
+import {
+ fetchList,
+ fetchCreateByObject,
+ fetchDeleteById,
+ fetchUpdateByObject,
+ fetchDetailById,
+ resetGroupCreateState
+} from "../redux/actions";
+import {
+ getUsers,
+ getGroupFormValues,
+ getTimeFetchedUsers,
+ isFetchingUsers,
+ isCreatingGroup,
+ getCreateGroupTimeFetched,
+ getCreateGroupError,
+ isEditedGroup,
+ getGroupById,
+ isLoadedGroups,
+ isLoadedGroup,
+ isGroupWithErrors
+} from "../redux/selectors";
+import Screen from "./Screen";
+import PropTypes from "prop-types";
+import { entity } from "../lib/entity";
+import { getContacts } from "../redux/selectors/contactsSelectors";
+
+const styles = theme => ({
+ progress: {
+ margin: theme.spacing.unit * 2
+ },
+ progressContainer: {
+ width: "100%",
+ flex: 1,
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "center"
+ }
+});
+
+export class GroupCreateScreen extends Component {
+ state = {
+ singlePermission: "",
+ multiUsers: [],
+ multiContacts: [],
+ multiResponsibles: [],
+ groupName: "",
+ groupDescription: "",
+ setFormValues: false
+ };
+
+ handleChange = name => value => {
+ this.setState({
+ [name]: !!value ? value.split(",").map(v => parseInt(v, 10)) : []
+ });
+ };
+
+ handleChangeSingle = name => value => {
+ this.setState({
+ [name]: value
+ });
+ };
+
+ constructor(props) {
+ super(props);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleDelete = this.handleDelete.bind(this);
+ }
+
+ componentWillMount = () => {
+ const { fetchUsers, fetchContacts, id, fetchGroup } = this.props;
+ fetchUsers({});
+ fetchContacts({});
+ if (!!id || id === 0) {
+ fetchGroup(id);
+ }
+ };
+
+ componentWillUnmount = () => {
+ this.setState({
+ groupName: "",
+ singlePermission: "",
+ multiUsers: [],
+ multiContacts: [],
+ multiResponsibles: [],
+ setFormValues: false
+ });
+ const { resetGroupCreateState } = this.props;
+ resetGroupCreateState();
+ };
+
+ componentWillReceiveProps = props => {
+ const { id, groupForEdit, isLoadedGroup, isLoadedGroups } = props;
+ const { setFormValues } = this.state;
+
+ if (
+ (parseInt(id, 10) || parseInt(id, 10) === 0) &&
+ !!isLoadedGroup &&
+ !!isLoadedGroups &&
+ !setFormValues
+ ) {
+ this.setState({
+ groupName: groupForEdit.name || "",
+ singlePermission: groupForEdit.permission || "",
+ multiUsers: groupForEdit.users || [],
+ multiContacts: groupForEdit.contacts || [],
+ multiResponsibles: groupForEdit.responsibles || [],
+ setFormValues: true
+ });
+ }
+ };
+
+ handleDelete = () => {
+ const { fetchDeleteGroup, id } = this.props;
+ fetchDeleteGroup(id);
+ };
+
+ handleSubmit = () => {
+ const {
+ fetchCreateGroup,
+ fetchEditGroup,
+ values: { name },
+ id
+ } = this.props;
+
+ const {
+ singlePermission,
+ multiUsers,
+ multiContacts,
+ multiResponsibles
+ } = this.state;
+
+ !!(id || id === 0)
+ ? fetchEditGroup({
+ id,
+ name,
+ permission: singlePermission,
+ users: multiUsers,
+ contacts: multiContacts,
+ responsibles: multiResponsibles
+ })
+ : fetchCreateGroup({
+ name,
+ permission: singlePermission,
+ users: multiUsers,
+ contacts: multiContacts,
+ responsibles: multiResponsibles
+ });
+ };
+
+ //todo: add canSubmit
+ render() {
+ const {
+ users,
+ contacts,
+ isLoaded,
+ classes,
+ isSent,
+ isEdited,
+ isGroupWithErrors,
+ values,
+ id,
+ t
+ } = this.props;
+ const {
+ groupName,
+ groupDescription,
+ multiUsers,
+ multiContacts,
+ multiResponsibles,
+ singlePermission
+ } = this.state;
+
+ var alert = null;
+ if (!!id) {
+ alert = isEdited ? (
+
+ {t("group.group_is_edited_msg")}
+
+ ) : null;
+ } else {
+ alert = isEdited ? (
+
+ {t("group.group_created_msg")}
+
+ ) : null;
+ }
+
+ const mappedUsers = (users || []).map(user => {
+ return { value: user.id, label: user.username };
+ });
+
+ const mappedContacts = (contacts || []).map(contact => {
+ return {
+ value: contact.id,
+ label: contact.lastName + ", " + contact.firstName
+ };
+ });
+
+ const mappedPermission = permission.map(permission => {
+ return { value: permission.value, label: permission.label };
+ });
+
+ const loadingScreen = (
+
+
+
+
+
+ );
+
+ if (id) {
+ if (!isLoaded) {
+ return loadingScreen;
+ }
+ }
+
+ if (!!id && !isSent && !isLoaded) {
+ return loadingScreen;
+ }
+
+ const canSubmit = values && values.name ? true : false;
+
+ return (
+
+ (this.form = form)}
+ onSubmit={this.handleSubmit}
+ canSubmit={canSubmit}
+ id={id}
+ />
+ {alert}
+ {t("group.error_message")}}
+ />
+
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ //todo remove parseInts from above
+ const id = parseInt(ownProps.match.params.id, 10);
+ const isFetching = isFetchingUsers(state);
+ //todo replace with timeFetchedUsers
+ const timeFetchedUsers = getTimeFetchedUsers(state);
+ const isCreating = isCreatingGroup(state);
+ const isEdited = isEditedGroup(state);
+ const groupCreatedTime = getCreateGroupTimeFetched(state);
+ const createGroupError = getCreateGroupError(state);
+ return {
+ users: getUsers(state),
+ contacts: getContacts(state),
+ group: state.group.create,
+ values: getGroupFormValues(state),
+ isFetching,
+ isLoaded: !isFetching && !!timeFetchedUsers,
+ isSent: !isCreating && !!groupCreatedTime && !createGroupError,
+ isEdited,
+ id,
+ groupForEdit: getGroupById(id)(state),
+ isLoadedGroups: isLoadedGroups(state),
+ isLoadedGroup: isLoadedGroup(state),
+ isGroupWithErrors: isGroupWithErrors(state)
+ };
+};
+
+const mapDispatchToProps = {
+ fetchUsers: fetchList(entity.user),
+ fetchContacts: fetchList(entity.contact),
+ fetchCreateGroup: fetchCreateByObject(entity.group),
+ fetchGroup: fetchDetailById(entity.group),
+ fetchEditGroup: fetchUpdateByObject(entity.group),
+ resetGroupCreateState,
+ fetchDeleteGroup: fetchDeleteById(entity.group)
+};
+
+GroupCreateScreen.propTypes = {
+ t: PropTypes.func.isRequired
+};
+
+export const ConnectedCreateScreen = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(GroupCreateScreen);
+
+export default withStyles(styles)(translate()(ConnectedCreateScreen));
diff --git a/src/components/GroupList.js b/src/components/GroupList.js
new file mode 100644
index 0000000..3089a86
--- /dev/null
+++ b/src/components/GroupList.js
@@ -0,0 +1,327 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Link } from "react-router-dom";
+import { withStyles } from "material-ui/styles";
+import Table, {
+ TableBody,
+ TableCell,
+ TableFooter,
+ TablePagination,
+ TableRow,
+ TableHead
+} from "material-ui/Table";
+import Paper from "material-ui/Paper";
+import IconButton from "material-ui/IconButton";
+import FirstPageIcon from "material-ui-icons/FirstPage";
+import KeyboardArrowLeft from "material-ui-icons/KeyboardArrowLeft";
+import KeyboardArrowRight from "material-ui-icons/KeyboardArrowRight";
+import LastPageIcon from "material-ui-icons/LastPage";
+import Screen from "./Screen";
+import { getGroupById, getGroups, getUsers } from "../redux/selectors";
+import { fetchDeleteById, fetchList } from "../redux/actions";
+import { entity } from "../lib/entity";
+import { connect } from "react-redux";
+import { translate } from "react-i18next";
+import CreateIcon from "material-ui-icons/Create";
+import DeleteIcon from "material-ui-icons/Delete";
+import Typography from "material-ui/Typography";
+import { getContacts } from "../redux/selectors/contactsSelectors";
+
+const actionsStyles = theme => ({
+ root: {
+ flexShrink: 0,
+ color: theme.palette.text.secondary,
+ marginLeft: theme.spacing.unit * 2.5
+ }
+});
+
+class TablePaginationActions extends React.Component {
+ handleFirstPageButtonClick = event => {
+ this.props.onChangePage(event, 0);
+ };
+
+ handleBackButtonClick = event => {
+ this.props.onChangePage(event, this.props.page - 1);
+ };
+
+ handleNextButtonClick = event => {
+ this.props.onChangePage(event, this.props.page + 1);
+ };
+
+ handleLastPageButtonClick = event => {
+ this.props.onChangePage(
+ event,
+ Math.max(0, Math.ceil(this.props.count / this.props.rowsPerPage) - 1)
+ );
+ };
+
+ render() {
+ const { classes, count, page, rowsPerPage, theme } = this.props;
+
+ return (
+
+
+ {theme.direction === "rtl" ? : }
+
+
+ {theme.direction === "rtl" ? (
+
+ ) : (
+
+ )}
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="Next Page"
+ >
+ {theme.direction === "rtl" ? (
+
+ ) : (
+
+ )}
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="Last Page"
+ >
+ {theme.direction === "rtl" ? : }
+
+
+ );
+ }
+}
+
+TablePaginationActions.propTypes = {
+ classes: PropTypes.object.isRequired,
+ count: PropTypes.number.isRequired,
+ onChangePage: PropTypes.func.isRequired,
+ page: PropTypes.number.isRequired,
+ rowsPerPage: PropTypes.number.isRequired,
+ theme: PropTypes.object.isRequired
+};
+
+const TablePaginationActionsWrapped = withStyles(actionsStyles, {
+ withTheme: true
+})(TablePaginationActions);
+
+const styles = theme => ({
+ root: {
+ width: "100%",
+ marginTop: theme.spacing.unit * 3,
+ overflowX: "auto",
+ padding: "24px",
+ "flex-grow": "1",
+ "background-color": "#fafafa"
+ },
+ table: {
+ minWidth: "20%",
+ maxWidth: "90%"
+ },
+ tableWrapper: {
+ overflowX: "auto",
+ padding: "24px",
+ "flex-grow": "1",
+ "background-color": "#fafafa"
+ }
+});
+
+class GroupList extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.handleDelete = this.handleDelete.bind(this);
+
+ this.state = {
+ page: 0,
+ rowsPerPage: 5
+ };
+ }
+
+ componentWillMount = () => {
+ const { fetchGroups, fetchUsers, fetchContacts } = this.props;
+ fetchGroups();
+ fetchUsers();
+ fetchContacts();
+ };
+
+ handleChangePage = (event, page) => {
+ this.setState({ page });
+ };
+
+ handleChangeRowsPerPage = event => {
+ this.setState({ rowsPerPage: event.target.value });
+ };
+
+ handleDelete = id => {
+ const { fetchDeleteGroup, fetchGroups } = this.props;
+ fetchDeleteGroup(id);
+ fetchGroups();
+ };
+
+ render() {
+ const { classes, groups, t, users, contacts } = this.props;
+ const { rowsPerPage, page } = this.state;
+ const emptyRows =
+ rowsPerPage -
+ Math.min(rowsPerPage, Object.keys(groups).length - page * rowsPerPage);
+
+ const newGroups = groups.map(group =>
+ Object.assign({}, group, {
+ users: !!group.users
+ ? users.filter(u => group.users.includes(u.id))
+ : [],
+ contacts: !!group.contacts
+ ? contacts.filter(contact => group.contacts.includes(contact.id))
+ : [],
+ responsibles: !!group.responsibles
+ ? contacts.filter(resp => group.responsibles.includes(resp.id))
+ : []
+ })
+ );
+
+ return (
+
+
+ {t("group_list.header")}
+
+
+
+
+
+
+ {t("group_list.name")}
+ {t("group_list.permission")}
+
+ {t("group_list.users")}
+
+
+ {t("group_list.contacts")}
+
+
+ {t("group_list.responsibles")}
+
+ {t("group_list.edit")}
+ {t("group_list.delete")}
+
+
+
+ {newGroups
+ .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+ .map(group => {
+ return (
+
+ {group.name}
+ {group.permission}
+
+ {group.users ? (
+
+ {group.users.map(user => {
+ return
{user.username}
;
+ })}
+
+ ) : null}
+
+
+ {group.contacts ? (
+
+ {group.contacts.map(contact => {
+ return (
+
{`${contact.lastName}, ${
+ contact.firstName
+ }`}
+ );
+ })}
+
+ ) : null}
+
+
+ {group.responsibles ? (
+
+ {group.responsibles.map(responsibility => {
+ return (
+
{`${
+ responsibility.lastName
+ }, ${responsibility.firstName}`}
+ );
+ })}
+
+ ) : null}
+
+
+
+
+
+
+
+ this.handleDelete(group.id)}
+ >
+
+
+
+
+ );
+ })}
+ {emptyRows > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ groups: getGroups(state),
+ users: getUsers(state),
+ contacts: getContacts(state),
+ group: id => getGroupById(id)(state)
+ };
+};
+
+const mapDispatchToProps = {
+ fetchGroups: fetchList(entity.group),
+ fetchUsers: fetchList(entity.user),
+ fetchContacts: fetchList(entity.contact),
+ fetchDeleteGroup: fetchDeleteById(entity.group)
+};
+
+GroupList.propTypes = {
+ classes: PropTypes.object.isRequired,
+ t: PropTypes.func.isRequired
+};
+
+export const ConnectedGroupList = connect(mapStateToProps, mapDispatchToProps)(
+ GroupList
+);
+
+export default withStyles(styles)(translate()(ConnectedGroupList));
diff --git a/src/components/LandingPage.js b/src/components/LandingPage.js
new file mode 100644
index 0000000..db6ab32
--- /dev/null
+++ b/src/components/LandingPage.js
@@ -0,0 +1,17 @@
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import { push } from "react-router-redux";
+import Screen from "./Screen";
+
+class LandingPage extends Component {
+ componentDidMount = () => {
+ const { push } = this.props;
+ push("/user/list");
+ };
+
+ render() {
+ return ;
+ }
+}
+
+export default connect(null, { push: push })(LandingPage);
diff --git a/src/components/Login.js b/src/components/Login.js
new file mode 100644
index 0000000..10b406b
--- /dev/null
+++ b/src/components/Login.js
@@ -0,0 +1,92 @@
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import PropTypes from "prop-types";
+import { translate } from "react-i18next";
+import { Snackbar } from "material-ui";
+
+import LoginForm from "./LoginForm";
+import { fetchLogin } from "../redux/actions";
+import { getLoginFormValues, getLoginError } from "../redux/selectors";
+
+export class Login extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: null
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { error } = this.props;
+ if (error !== nextProps.error) {
+ this.setState({
+ error
+ });
+ }
+ }
+
+ handleClose = () => {
+ this.setState({ error: null });
+ };
+
+ submit = values => {
+ const { fetchLogin, values: { username, password } } = this.props;
+ fetchLogin(username, password);
+ };
+
+ render() {
+ const { values } = this.props;
+ const { error } = this.state;
+
+ const canSubmit =
+ values && values.username && values.password ? true : false;
+
+ return (
+
+ (this.form = form)}
+ initialValues={{ username: "", password: "" }}
+ onSubmit={this.submit}
+ canSubmit={canSubmit}
+ />
+ Failed to Login}
+ />
+
+ );
+ }
+}
+
+Login.propTypes = {
+ t: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => {
+ return {
+ values: getLoginFormValues(state),
+ error: getLoginError(state)
+ };
+};
+
+const mapDispatchToProps = {
+ fetchLogin
+};
+/*
+mapDispatchToProps above does the same like:
+const mapDispatchToProps = dispatch => ({
+ fetchLogin: () => dispatch(fetchLogin())
+});
+ */
+
+export const ConnectedLogin = connect(mapStateToProps, mapDispatchToProps)(
+ Login
+);
+
+export default translate()(ConnectedLogin);
diff --git a/src/components/Login.test.js b/src/components/Login.test.js
new file mode 100644
index 0000000..4751dc1
--- /dev/null
+++ b/src/components/Login.test.js
@@ -0,0 +1,16 @@
+import React from "react";
+import { shallow } from "enzyme";
+import { Login } from "./Login";
+
+it("renders without crashing", () => {
+ shallow();
+});
+
+it("has exactly one LoginForm", () => {
+ const component = shallow();
+ const initialValues = component
+ .find("ReduxForm")
+ .map(form => form.prop("initialValues"));
+ expect(initialValues).toHaveLength(1);
+ expect(initialValues[0]).toEqual({ username: "", password: "" });
+});
diff --git a/src/components/LoginForm.js b/src/components/LoginForm.js
new file mode 100644
index 0000000..b18c508
--- /dev/null
+++ b/src/components/LoginForm.js
@@ -0,0 +1,77 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "material-ui/styles";
+import { TextField, Button } from "material-ui";
+import { Field, reduxForm } from "redux-form";
+import { translate } from "react-i18next";
+
+const styles = theme => ({
+ loginForm: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center"
+ },
+ formItem: {
+ margin: theme.spacing.unit
+ },
+ formColumn: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "flex-start"
+ },
+ formRow: {
+ display: "flex",
+ flexDirection: "row"
+ }
+});
+
+const renderTextField = ({ input, meta: { touched, error }, ...rest }) => (
+
+);
+
+export class LoginForm extends Component {
+ render() {
+ const { handleSubmit, canSubmit, classes, t } = this.props;
+ return (
+
+ );
+ }
+}
+
+LoginForm.propTypes = {
+ classes: PropTypes.object.isRequired,
+ t: PropTypes.func.isRequired
+};
+
+export default reduxForm({ form: "login" })(
+ withStyles(styles)(translate()(LoginForm))
+);
diff --git a/src/components/MenuAppBar.js b/src/components/MenuAppBar.js
new file mode 100644
index 0000000..3616c40
--- /dev/null
+++ b/src/components/MenuAppBar.js
@@ -0,0 +1,118 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { withStyles } from "material-ui/styles";
+import AppBar from "material-ui/AppBar";
+import { Toolbar } from "material-ui";
+import Typography from "material-ui/Typography";
+import IconButton from "material-ui/IconButton";
+import MenuIcon from "material-ui-icons/Menu";
+import AccountCircle from "material-ui-icons/AccountCircle";
+import Menu, { MenuItem } from "material-ui/Menu";
+
+import { logout } from "../redux/actions";
+
+const styles = theme => ({
+ flex: {
+ flex: 1
+ },
+ menuButton: {
+ marginLeft: -12,
+ marginRight: 20
+ }
+});
+
+class MenuAppBar extends React.Component {
+ state = {
+ auth: true,
+ anchorEl: null
+ };
+
+ handleChange = (event, checked) => {
+ this.setState({ auth: checked });
+ };
+
+ handleMenu = event => {
+ this.setState({ anchorEl: event.currentTarget });
+ };
+
+ handleClose = () => {
+ this.setState({ anchorEl: null });
+ };
+
+ handleLogout = () => {
+ const { logout } = this.props;
+ logout();
+ this.handleClose();
+ };
+
+ render() {
+ const { classes } = this.props;
+ const { auth, anchorEl } = this.state;
+ const open = Boolean(anchorEl);
+
+ return (
+
+
+
+
+
+
+ Mitgliederverwaltung
+
+ {auth && (
+
+ )}
+
+
+ );
+ }
+}
+
+MenuAppBar.propTypes = {
+ classes: PropTypes.object.isRequired
+};
+
+const mapStateToProps = state => {
+ return {};
+};
+
+const mapDispatchToProps = {
+ logout
+};
+
+const ConnectedMenuAppBar = connect(mapStateToProps, mapDispatchToProps)(
+ MenuAppBar
+);
+
+export default withStyles(styles)(ConnectedMenuAppBar);
diff --git a/src/components/ReactSelect.js b/src/components/ReactSelect.js
new file mode 100644
index 0000000..94cf945
--- /dev/null
+++ b/src/components/ReactSelect.js
@@ -0,0 +1,242 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "material-ui/styles";
+import Typography from "material-ui/Typography";
+import Input from "material-ui/Input";
+import { MenuItem } from "material-ui/Menu";
+import ArrowDropDownIcon from "material-ui-icons/ArrowDropDown";
+import CancelIcon from "material-ui-icons/Cancel";
+import ArrowDropUpIcon from "material-ui-icons/ArrowDropUp";
+import ClearIcon from "material-ui-icons/Clear";
+import Chip from "material-ui/Chip";
+import Select from "react-select";
+import "react-select/dist/react-select.css";
+import { translate } from "react-i18next";
+
+const ITEM_HEIGHT = 48;
+
+const styles = theme => ({
+ // We had to use a lot of global selectors in order to style react-select.
+ // We are waiting on https://github.com/JedWatson/react-select/issues/1679
+ // to provide a better implementation.
+ // Also, we had to reset the default style injected by the library.
+ "@global": {
+ ".Select-control": {
+ display: "flex",
+ alignItems: "center",
+ border: 0,
+ height: "auto",
+ background: "transparent",
+ "&:hover": {
+ boxShadow: "none"
+ }
+ },
+ ".Select-multi-value-wrapper": {
+ flexGrow: 1,
+ display: "flex",
+ flexWrap: "wrap"
+ },
+ ".Select--multi": {
+ height: "auto"
+ },
+ ".Select--multi .Select-input": {
+ margin: 0
+ },
+ ".Select.has-value.is-clearable.Select--single > .Select-control .Select-value": {
+ padding: 0
+ },
+ ".Select-noresults": {
+ padding: theme.spacing.unit * 2
+ },
+ ".Select-input": {
+ display: "inline-flex !important",
+ padding: 0,
+ height: "auto"
+ },
+ ".Select-input input": {
+ background: "transparent",
+ border: 0,
+ padding: 0,
+ cursor: "default",
+ display: "inline-block",
+ fontFamily: "inherit",
+ fontSize: "inherit",
+ margin: 0,
+ outline: 0
+ },
+ ".Select-placeholder, .Select--single .Select-value": {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ display: "flex",
+ alignItems: "center",
+ fontFamily: theme.typography.fontFamily,
+ fontSize: theme.typography.pxToRem(16),
+ padding: 0
+ },
+ ".Select-placeholder": {
+ opacity: 0.42,
+ color: theme.palette.common.black
+ },
+ ".Select--single > .Select-control .Select-value": {
+ color: theme.palette.common.black
+ },
+ ".Select-menu-outer": {
+ backgroundColor: theme.palette.background.paper,
+ boxShadow: theme.shadows[2],
+ position: "absolute",
+ left: 0,
+ top: `calc(100% + ${theme.spacing.unit}px)`,
+ width: "100%",
+ zIndex: 2,
+ maxHeight: ITEM_HEIGHT * 4.5
+ },
+ ".Select.is-focused:not(.is-open) > .Select-control": {
+ boxShadow: "none"
+ },
+ ".Select-menu": {
+ maxHeight: ITEM_HEIGHT * 4.5,
+ overflowY: "auto"
+ },
+ ".Select-menu div": {
+ boxSizing: "content-box"
+ },
+ ".Select-arrow-zone, .Select-clear-zone": {
+ color: theme.palette.action.active,
+ cursor: "pointer",
+ height: 21,
+ width: 21,
+ zIndex: 1
+ },
+ // Only for screen readers. We can't use display none.
+ ".Select-aria-only": {
+ position: "absolute",
+ overflow: "hidden",
+ clip: "rect(0 0 0 0)",
+ height: 1,
+ width: 1,
+ margin: -1
+ }
+ }
+});
+
+class Option extends React.Component {
+ handleClick = event => {
+ this.props.onSelect(this.props.option, event);
+ };
+
+ render() {
+ const { children, isFocused, isSelected, onFocus } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+function SelectWrapped(props) {
+ const { classes, ...other } = props;
+
+ return (
+