13 Commits

Author SHA1 Message Date
Leonard Krause
ed2de23a3e deny deletion of admin accounts 2018-04-27 10:50:35 +02:00
Leonard Krause
cd2e90d974 added profile user screen and contact 2018-04-27 10:28:41 +02:00
Leonard Krause
586bff6e58 more layout fixes 2018-04-27 10:12:53 +02:00
Leonard Krause
4c0782456e use fetchList only for lists 2018-04-27 10:04:34 +02:00
Leonard Krause
d9ba634300 small layout fixes 2018-04-27 09:49:05 +02:00
Leonard Krause
d06f43ff0d users liste 2018-04-27 09:39:43 +02:00
Leonard Krause
5087bf359b Contact List 2018-04-27 09:26:20 +02:00
Leonard Krause
4f9b08ed11 small layout fix 2018-04-27 08:16:15 +02:00
Leonard Krause
8ca8ee061f finish new responsive contact form 2018-04-27 08:09:55 +02:00
Leonard Krause
5b6de60cfa contact create and edit screen with validations 2018-04-27 05:40:36 +02:00
Leonard Krause
7e0815b320 updated ci status in readme 2018-04-27 03:07:29 +02:00
Leonard Krause
3270c4b1c0 updated ci status in readme 2018-04-27 03:06:28 +02:00
Leonard Krause
8bd0d5bdbb started Contact Screens 2018-04-27 03:04:04 +02:00
34 changed files with 1292 additions and 513 deletions

View File

@@ -1,7 +1,6 @@
## Status
Tests: [![Build Status](https://ci.net1.leoek.eu/buildStatus/icon?job=swt/webclient-test)](https://ci.net1.leoek.eu/job/swt/webclient-test)
Build: [![Build Status](https://ci.net1.leoek.eu/buildStatus/icon?job=swt/webclient-build)](https://ci.net1.leoek.eu/job/swt/webclient)
[![Build Status](https://ci.net1.leoek.eu/buildStatus/icon?job=swt/verver-client)](https://ci.net1.leoek.eu/job/swt/verver-client)
## Run

View File

@@ -10,7 +10,7 @@
"i18next-xhr-backend": "^1.5.0",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.4",
"material-ui": "^1.0.0-beta.35",
"material-ui": "^1.0.0-beta.43",
"material-ui-icons": "^1.0.0-beta.17",
"mdi-react": "^2.1.19",
"react": "^16.2.0",

View File

@@ -17,14 +17,14 @@ import configureStore from "./redux/createStore";
import { MuiThemeProvider, createMuiTheme } from "material-ui/styles";
import Login from "./components/Login";
import UserList from "./components/UserList";
import UserList from "./components/user/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";
import { ContactListScreen, ContactDetailScreen } from "./components/contact";
const { store, history, persistor } = configureStore();
const theme = createMuiTheme({
@@ -56,7 +56,27 @@ class App extends Component {
<div>
<Route exact path="/" component={LandingPage} />
<Route exact path="/login" component={Login} />
<Route exact path="/contact/list" component={ContactList} />
<Route
exact
path="/contact/list"
component={ContactListScreen}
/>
<Route
exact
path="/contact/:id/:type"
component={ContactDetailScreen}
/>
<Route exact path="/profile" component={UserCreateScreen} />
<Route
exact
path="/profile"
component={ContactDetailScreen}
/>
<Route
exact
path="/contact/create"
component={ContactDetailScreen}
/>
<Route exact path="/user/list" component={UserList} />
<Route
exact
@@ -68,7 +88,6 @@ class App extends Component {
path="/user/create"
component={UserCreateScreen}
/>
<Route exact path="/profile" component={UserCreateScreen} />
<Route exact path="/role/list" component={RoleList} />
<Route
exact

View File

@@ -1,207 +0,0 @@
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 (
<div className={classes.root}>
<IconButton
onClick={this.handleBackButtonClick}
disabled={page === 0}
aria-label="Previous Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={this.handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="Next Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
</div>
);
}
}
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 (
<Screen>
<h2 align="center">Übersicht aller Mitglieder</h2>
{users ? (
<div>
<Table className={classes.table} align="center">
<TableHead>
<TableRow>
<TableCell numeric>Mitgliedsnummer</TableCell>
<TableCell>Name</TableCell>
<TableCell>Email</TableCell>
<TableCell style={{ width: 250 }}>Rollen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map(user => {
return (
<TableRow key={user.id}>
<TableCell numeric>{user.id}</TableCell>
<TableCell>
{user.firstName} {user.lastName}
</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
{user.roles ? (
<div>
{user.roles.map(role => {
return <p key={role.id}>{role.name}</p>;
})}
</div>
) : null}
</TableCell>
</TableRow>
);
})}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
colSpan={3}
count={users.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={this.handleChangePage}
onChangeRowsPerPage={this.handleChangeRowsPerPage}
Actions={TablePaginationActionsWrapped}
/>
</TableRow>
</TableFooter>
</Table>
</div>
) : (
<div align="center">Es existieren keine Mitglieder!</div>
)}
</Screen>
);
}
}
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);

View File

@@ -6,15 +6,14 @@ import { permission } from "../config/groupPermissionTypes";
import GroupCreateForm from "./GroupCreateForm";
import {
fetchList,
fetchCreateByObject,
fetchDeleteById,
fetchUpdateByObject,
fetchDetailById,
fetchAll,
resetGroupCreateState
} from "../redux/actions";
import {
getUsers,
getGroupFormValues,
getTimeFetchedUsers,
isFetchingUsers,
@@ -25,12 +24,12 @@ import {
getGroupById,
isLoadedGroups,
isLoadedGroup,
isGroupWithErrors
isGroupWithErrors,
getEntityItems
} 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: {
@@ -274,8 +273,8 @@ const mapStateToProps = (state, ownProps) => {
const groupCreatedTime = getCreateGroupTimeFetched(state);
const createGroupError = getCreateGroupError(state);
return {
users: getUsers(state),
contacts: getContacts(state),
users: getEntityItems(entity.user)(state),
contacts: getEntityItems(entity.contact)(state),
group: state.group.create,
values: getGroupFormValues(state),
isFetching,
@@ -291,8 +290,8 @@ const mapStateToProps = (state, ownProps) => {
};
const mapDispatchToProps = {
fetchUsers: fetchList(entity.user),
fetchContacts: fetchList(entity.contact),
fetchUsers: fetchAll(entity.user),
fetchContacts: fetchAll(entity.contact),
fetchCreateGroup: fetchCreateByObject(entity.group),
fetchGroup: fetchDetailById(entity.group),
fetchEditGroup: fetchUpdateByObject(entity.group),

View File

@@ -1,6 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
import { Card, CardContent, Typography } from "material-ui";
import { withStyles } from "material-ui/styles";
import Table, {
TableBody,
@@ -10,7 +11,6 @@ import Table, {
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";
@@ -24,7 +24,6 @@ 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 => ({
@@ -190,10 +189,15 @@ class GroupList extends React.Component {
return (
<Screen>
<Typography type="headline" component="h2" align="center">
{t("group_list.header")}
</Typography>
<Paper className={classes.root}>
<Card className={classes.root}>
<CardContent>
<Typography className={classes.title} color="textSecondary">
{t(`group.listScreen.title`)}
</Typography>
<Typography variant="headline" component="h2">
{t(`group.listScreen.headline`)}
</Typography>
</CardContent>
<div className={classes.tableWrapper}>
<Table className={classes.table} align="center">
<TableHead>
@@ -293,7 +297,7 @@ class GroupList extends React.Component {
</TableFooter>
</Table>
</div>
</Paper>
</Card>
</Screen>
);
}

View File

@@ -234,7 +234,6 @@ export class ReactSelect extends React.Component {
ReactSelect.propTypes = {
classes: PropTypes.object.isRequired,
multi: PropTypes.object,
options: PropTypes.array.isRequired,
handleChangeMulti: PropTypes.func.isRequired
};

View File

@@ -9,12 +9,11 @@ import {
fetchDeleteById,
fetchUpdateByObject,
fetchDetailById,
fetchList,
fetchAll,
resetRoleCreateState
} from "../redux/actions";
import {
getPermissions,
getUsers,
getRoleFormValues,
getTimeFetchedPermissions,
isFetchingPermissions,
@@ -25,7 +24,8 @@ import {
isLoadedRole,
getRoleById,
isLoadedRoles,
isRoleWithErrors
isRoleWithErrors,
getEntityItems
} from "../redux/selectors";
import Screen from "./Screen";
import { entity } from "../lib/entity";
@@ -241,7 +241,7 @@ const mapStateToProps = (state, ownProps) => {
return {
permissions: getPermissions(state),
users: getUsers(state),
users: getEntityItems(entity.user)(state),
role: state.role.create,
roleForEdit: getRoleById(id)(state),
values: getRoleFormValues(state),
@@ -257,10 +257,10 @@ const mapStateToProps = (state, ownProps) => {
};
const mapDispatchToProps = {
fetchPermissions: fetchList(entity.permission),
fetchUsers: fetchList(entity.user),
fetchPermissions: fetchAll(entity.permission),
fetchUsers: fetchAll(entity.user),
fetchCreateRole: fetchCreateByObject(entity.role),
fetchRoles: fetchList(entity.role),
fetchRoles: fetchAll(entity.role),
fetchRole: fetchDetailById(entity.role),
fetchEditRole: fetchUpdateByObject(entity.role),
fetchDeleteRole: fetchDeleteById(entity.role),

View File

@@ -10,7 +10,7 @@ import Table, {
TableRow,
TableHead
} from "material-ui/Table";
import Paper from "material-ui/Paper";
import { Card, CardContent, Typography } from "material-ui";
import IconButton from "material-ui/IconButton";
import FirstPageIcon from "material-ui-icons/FirstPage";
import KeyboardArrowLeft from "material-ui-icons/KeyboardArrowLeft";
@@ -30,7 +30,6 @@ 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 { entity } from "../lib/entity";
import { CircularProgress } from "material-ui";
@@ -205,10 +204,15 @@ class RoleList extends React.Component {
return (
<Screen>
<Typography type="headline" component="h2" align="center">
{t("role_list.header")}
</Typography>
<Paper className={classes.root}>
<Card className={classes.root}>
<CardContent>
<Typography className={classes.title} color="textSecondary">
{t(`role.listScreen.title`)}
</Typography>
<Typography variant="headline" component="h2">
{t(`role.listScreen.headline`)}
</Typography>
</CardContent>
<div className={classes.tableWrapper}>
<Table className={classes.table} align="center">
<TableHead>
@@ -288,7 +292,7 @@ class RoleList extends React.Component {
</TableFooter>
</Table>
</div>
</Paper>
</Card>
</Screen>
);
}

View File

@@ -93,9 +93,18 @@ const styles = theme => {
...theme.mixins.toolbar
},
content: {
display: "flex",
flexGrow: 1,
maxWidth: "100%",
backgroundColor: theme.palette.background.default,
padding: theme.spacing.unit * 3
padding: theme.spacing.unit * 5,
paddingTop: 64 + theme.spacing.unit * 5
},
contentDrawerOpen: {
marginLeft: drawerWidth - 16
},
contentDrawerClosed: {
marginLeft: closedDrawerWidth - 16
},
noTextDecoration: {
textDecoration: "none"
@@ -231,8 +240,14 @@ export class Screen extends React.Component {
open={open}
/>
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
<div className={classes.toolbar} />
<main
className={classNames(
classes.content,
open ? classes.contentDrawerOpen : classes.contentDrawerClosed
)}
>
{children}
</main>
</div>

View File

@@ -77,7 +77,8 @@ export class UserCreateForm extends React.Component {
checkedEnabled,
canSubmit,
id,
t
t,
canDelete
} = this.props;
var validID = !!id || id === 0;
return (
@@ -151,7 +152,8 @@ export class UserCreateForm extends React.Component {
<Checkbox
checked={checkedAdmin}
name={"checkedAdmin"}
onChange={handleChangeCheckbox}
disabled={!canDelete}
onChange={canDelete ? handleChangeCheckbox : undefined}
label={t("user.checkBoxAdmin")}
/>
</div>
@@ -171,7 +173,7 @@ export class UserCreateForm extends React.Component {
<Button color="primary">{t("user.cancel")}</Button>
</Link>
</div>
{!!validID ? (
{!!validID && canDelete ? (
<div className={classes.buttonRow}>
<Button onClick={handleDelete} color="primary">
{t("user.delete")}

View File

@@ -2,22 +2,19 @@ import React, { Component } from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { CircularProgress, withStyles, Snackbar } from "material-ui";
import { get } from "lodash";
import UserCreateForm from "./UserCreateForm";
import {
fetchList,
fetchCreateByObject,
fetchDeleteById,
fetchUpdateByObject,
fetchDetailById,
fetchAll,
resetUserCreateState
} from "../redux/actions";
import {
getUsers,
getTimeFetchedUsers,
isFetchingUsers,
getGroups,
getRoles,
getUserFormValues,
isCreatingUser,
isEditedUser,
@@ -26,12 +23,13 @@ import {
getUserById,
isLoadedUsers,
isLoadedUser,
isUserWithErrors
isUserWithErrors,
getEntityItems,
getUserIdFromToken
} 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: {
@@ -89,10 +87,10 @@ export class UserCreateScreen extends Component {
fetchUser,
id
} = this.props;
fetchUsers({});
fetchContacts({});
fetchRoles({});
fetchGroups({});
fetchUsers();
fetchContacts();
fetchRoles();
fetchGroups();
if (!!id || id === 0) {
fetchUser(id);
}
@@ -192,7 +190,8 @@ export class UserCreateScreen extends Component {
isUserWithErrors,
values,
id,
t
t,
canDelete
} = this.props;
const {
username,
@@ -283,6 +282,7 @@ export class UserCreateScreen extends Component {
onSubmit={this.handleSubmit}
canSubmit={canSubmit}
id={id}
canDelete={canDelete}
/>
{alert}
<Snackbar
@@ -300,7 +300,10 @@ export class UserCreateScreen extends Component {
}
const mapStateToProps = (state, ownProps) => {
const id = parseInt(ownProps.match.params.id, 10);
const isProfile = get(ownProps, "match.path") === "/profile";
const id = isProfile
? getUserIdFromToken(state)
: parseInt(ownProps.match.params.id, 10);
const isFetching = isFetchingUsers(state);
//todo replace with timeFetchedUsers
const timeFetchedUsers = getTimeFetchedUsers(state);
@@ -309,19 +312,21 @@ const mapStateToProps = (state, ownProps) => {
const isEdited = isEditedUser(state);
const groupCreatedTime = getTimeFetchedUserList(state);
const createUserError = getCreateUserError(state);
const user = getUserById(id)(state);
const values = getUserFormValues(state);
return {
users: getUsers(state),
roles: getRoles(state),
groups: getGroups(state),
contacts: getContacts(state),
values: getUserFormValues(state),
users: getEntityItems(entity.user)(state),
roles: getEntityItems(entity.role)(state),
groups: getEntityItems(entity.group)(state),
contacts: getEntityItems(entity.contact)(state),
values,
canDelete: !(values || {}).admin && !(user || {}).admin,
isFetching,
isLoaded: !isFetching && !!timeFetchedUsers,
isSent: !isCreating && !!groupCreatedTime && !createUserError,
isEdited,
id,
userForEdit: getUserById(id)(state),
userForEdit: user,
isLoadedUsers: isLoadedUsers(state),
isLoadedUser: isLoadedUser(state),
isUserWithErrors: isUserWithErrors(state)
@@ -329,10 +334,10 @@ const mapStateToProps = (state, ownProps) => {
};
const mapDispatchToProps = {
fetchUsers: fetchList(entity.user),
fetchRoles: fetchList(entity.role),
fetchGroups: fetchList(entity.group),
fetchContacts: fetchList(entity.contact),
fetchUsers: fetchAll(entity.user),
fetchRoles: fetchAll(entity.role),
fetchGroups: fetchAll(entity.group),
fetchContacts: fetchAll(entity.contact),
fetchCreateUser: fetchCreateByObject(entity.user),
fetchUser: fetchDetailById(entity.user),

View File

@@ -1,199 +0,0 @@
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 { getUsers as getUsersFromState, getUserById } 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 { Link } from "react-router-dom";
import CreateIcon from "material-ui-icons/Create";
import { translate } from "react-i18next";
import Typography from "material-ui/Typography";
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 (
<div className={classes.root}>
<IconButton
onClick={this.handleBackButtonClick}
disabled={page === 0}
aria-label="Previous Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={this.handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="Next Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
</div>
);
}
}
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: {
width: "30%"
},
tableWrapper: {
overflowX: "auto"
}
});
class UserList extends Component {
componentWillMount = () => {
const { fetchUsers } = this.props;
fetchUsers();
};
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, t } = this.props;
const { rowsPerPage, page } = this.state;
//console.log("Render a component", users, this.props.user(6));
return (
<Screen>
<Typography variant="title" align="center" gutterBottom>
{t("user_list.header")}
</Typography>
{users ? (
<div>
<Table className={classes.table} align="center">
<TableHead>
<TableRow>
<TableCell>{t("user_list.name")}</TableCell>
<TableCell>{t("user_list.edit")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map(user => {
return (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>
<Link to={`/user/edit/${user.id}`}>
<CreateIcon />
</Link>
</TableCell>
</TableRow>
);
})}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
colSpan={3}
count={users.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={this.handleChangePage}
onChangeRowsPerPage={this.handleChangeRowsPerPage}
Actions={TablePaginationActionsWrapped}
/>
</TableRow>
</TableFooter>
</Table>
</div>
) : null}
</Screen>
);
}
}
UserList.propTypes = {
classes: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
const mapStateToProps = state => {
return {
users: getUsersFromState(state),
user: id => getUserById(id)(state)
};
};
const mapDispatchToProps = {
fetchUsers: fetchList(entity.user)
};
export const ConnectedUserList = connect(mapStateToProps, mapDispatchToProps)(
UserList
);
export default withStyles(styles)(translate()(ConnectedUserList));

View File

@@ -0,0 +1,226 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import {
withStyles,
Card,
CardContent,
Typography,
CircularProgress
} from "material-ui";
import PropTypes from "prop-types";
import { initialize } from "redux-form";
import { get } from "lodash";
import { goBack } from "react-router-redux";
import {
getContactById,
getEntityItems,
getContactFormValues,
getIsFetching,
getContactIdFromToken
} from "../../redux/selectors";
import { entity } from "../../lib/entity";
import { detailScreenType, apiMethod } from "../../config";
import Screen from "../Screen";
import ContactForm from "./ContactForm";
import {
fetchUpdateByObject,
fetchCreateByObject,
fetchDetailById,
fetchAll
} from "../../redux/actions";
const styles = theme => ({
progress: {
margin: theme.spacing.unit * 2
},
progressContainer: {
width: "100%",
flex: 1,
display: "flex",
flexDirection: "row",
justifyContent: "center"
},
card: {
height: "100%",
padding: theme.spacing.unit * 2
},
bullet: {
display: "inline-block",
margin: "0 2px",
transform: "scale(0.8)"
},
title: {
marginBottom: 16,
fontSize: 14
},
pos: {
marginBottom: 12
}
});
export class DetailScreen extends Component {
state = {
multiGroups: []
};
componentWillMount = () => {
const { fetchAllGroups } = this.props;
this.fetchContact();
fetchAllGroups();
};
fetchContact = () => {
const { fetchContactDetailById, type, id } = this.props;
if (type === detailScreenType.edit || type === detailScreenType.view) {
if (!!id || id === 0) {
fetchContactDetailById(id);
}
}
};
//TODO: fix the React Select and make it a proper redux forms field
handleChangeMultiGroups = multiGroups => {
this.setState({
multiGroups: !!multiGroups
? multiGroups.split(",").map(v => parseInt(v, 10))
: []
});
};
componentWillReceiveProps = nextProps => {
const { contact } = this.props;
if (contact !== nextProps.contact && nextProps.contact) {
this.setState({
multiGroups: nextProps.contact.groups
});
}
};
handleSubmit = values => {
const { multiGroups } = this.state;
const { type, fetchUpdateByObject, fetchCreateByObject } = this.props;
if (type === detailScreenType.edit) {
fetchUpdateByObject({
...values,
groups: multiGroups
});
return true;
}
if (type === detailScreenType.create) {
fetchCreateByObject({
...values,
groups: multiGroups
});
return true;
}
return false;
};
handleCancel = () => {
const { goBack } = this.props;
goBack();
return true;
};
render = () => {
const { multiGroups } = this.state;
console.log(multiGroups);
const { classes, t, contact, name, type, groups, isLoading } = this.props;
const optionsGroups = (groups || []).map(group => ({
value: group.id,
label: group.name
}));
if (isLoading) {
return (
<Screen>
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} />
</div>
</Screen>
);
}
return (
<Screen>
<div>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="textSecondary">
{t(`contact.detailScreen.title.${type}`)}
</Typography>
<Typography variant="headline" component="h2">
{t(`contact.detailScreen.headline.${type}`, { name })}
</Typography>
</CardContent>
<ContactForm
initialValues={contact}
initialize={initialize}
enableReinitialize={true}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
optionsGroups={optionsGroups}
multiGroups={multiGroups}
handleChangeMultiGroups={this.handleChangeMultiGroups}
/>
</Card>
</div>
</Screen>
);
};
}
const mapStateToProps = (state, ownProps) => {
const isProfile = get(ownProps, "match.path") === "/profile";
const id = isProfile
? getContactIdFromToken(state)
: get(ownProps, "match.params.id");
const type = get(ownProps, "match.params.type") || "create";
const contact = getContactById(id)(state);
const groups = getEntityItems(entity.group)(state);
const isFetchingContact = getIsFetching(apiMethod.detail)(entity.contact)(
state
);
const isFetchingGroups = getIsFetching(apiMethod.all)(entity.group)(state);
const values = getContactFormValues(state);
return {
isLoading: isFetchingContact || isFetchingGroups,
id,
type,
contact,
groups,
values,
name:
contact &&
contact.firstName &&
contact.lastName &&
`${contact.firstName} ${contact.lastName}`
};
};
const mapDispatchToProps = {
initialize,
goBack,
fetchCreateByObject: fetchCreateByObject(entity.contact),
fetchUpdateByObject: fetchUpdateByObject(entity.contact),
fetchContactDetailById: fetchDetailById(entity.contact),
fetchAllGroups: fetchAll(entity.group)
};
DetailScreen.propTypes = {
t: PropTypes.func.isRequired,
classes: PropTypes.object.isRequired,
initialize: PropTypes.func.isRequired
};
export const ConnectedDetailScreen = connect(
mapStateToProps,
mapDispatchToProps
)(DetailScreen);
export default withStyles(styles)(translate()(ConnectedDetailScreen));

View File

@@ -0,0 +1,142 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "material-ui/styles";
import { Button, CardContent, CardActions, Grid } from "material-ui";
import { reduxForm, Form } from "redux-form";
import { translate } from "react-i18next";
import { FormTextField, formValidations } from "../layout/FormFields";
import ReactSelect from "../ReactSelect";
const styles = theme => ({
form: {
display: "flex",
flexDirection: "column",
alignItems: "center",
flexGrow: 1
},
actions: {
width: "100%"
},
reactSelect: {
width: "100%"
},
formItem: {
margin: 8,
marginTop: 22
}
});
export class ContactForm extends Component {
render() {
const {
handleSubmit,
onCancel,
invalid,
submitting,
asyncValidating,
classes,
t,
optionsGroups = [],
handleChangeMultiGroups,
multiGroups
} = this.props;
const canSubmit = !(submitting || invalid || asyncValidating === true);
return (
<Form onSubmit={handleSubmit} className={classes.form}>
<CardContent>
<Grid container spacing={24}>
<Grid item xs={12} md={6} lg={3}>
<FormTextField
label={t("contact.label.email")}
name="email"
validate={[formValidations.required, formValidations.email]}
/>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<FormTextField
label={t("contact.label.firstname")}
name="firstName"
validate={[formValidations.required]}
/>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<FormTextField
label={t("contact.label.lastname")}
name="lastName"
validate={[formValidations.required]}
/>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<div className={classes.formItem}>
{/*TODO: fix the reactselect component to work with redux-forms.*/}
<ReactSelect
className={classes.reactSelect}
name={"selectGroups"}
options={optionsGroups}
handleChangeMulti={handleChangeMultiGroups}
multi={multiGroups}
canAddMultipleValues={true}
placeholder={t("contact.placeholder.groups")}
/>
</div>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<FormTextField
label={t("contact.label.phone")}
name="phone"
validate={[formValidations.phone]}
/>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<FormTextField
label={t("contact.label.address")}
name="address"
/>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<FormTextField
label={t("contact.label.bankDetails")}
name="bankDetails"
/>
</Grid>
</Grid>
</CardContent>
<CardActions className={classes.actions}>
<Grid
container
spacing={24}
alignItems={"flex-end"}
justify={"flex-end"}
>
<Grid item>
<Button color="primary" size="large" onClick={onCancel}>
{t("cancel")}
</Button>
<Button
color="primary"
type="submit"
disabled={!canSubmit}
size="large"
>
{t("contact.detailScreen.saveButtonLabel")}
</Button>
</Grid>
</Grid>
</CardActions>
</Form>
);
}
}
ContactForm.propTypes = {
classes: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool,
groups: PropTypes.array
};
export default reduxForm({ form: "contact" })(
withStyles(styles)(translate()(ContactForm))
);

View File

@@ -0,0 +1,294 @@
import React from "react";
import PropTypes from "prop-types";
import { withStyles } from "material-ui/styles";
import Table, {
TableHead,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow
} from "material-ui/Table";
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 CreateIcon from "material-ui-icons/Create";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { Link } from "react-router-dom";
import { CircularProgress, Card, CardContent, Typography } from "material-ui";
import { apiMethod } from "../../config";
import { entity } from "../../lib/entity";
import {
getMeta,
getItems,
getIsFetching,
getTimeFetched
} from "../../redux/selectors";
import { fetchList } from "../../redux/actions";
import Screen from "../Screen";
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, this.props.totalPages - 1);
};
render() {
const { classes, page, theme, meta } = this.props;
return (
<div className={classes.root}>
<IconButton
onClick={this.handleFirstPageButtonClick}
disabled={page === 0}
aria-label="First Page"
>
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={this.handleBackButtonClick}
disabled={page === 0}
aria-label="Previous Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={this.handleNextButtonClick}
disabled={!meta.hasNext}
aria-label="Next Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
<IconButton
onClick={this.handleLastPageButtonClick}
disabled={page === meta.totalPages - 1}
aria-label="Last Page"
>
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</div>
);
}
}
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 ConnectedTablePaginationActionsWrapped = connect(
state => ({ meta: getMeta(apiMethod.list)(entity.contact)(state) }),
null
)(TablePaginationActionsWrapped);
const styles = theme => ({
progress: {
margin: theme.spacing.unit * 2
},
progressContainer: {
width: "100%",
flex: 1,
display: "flex",
flexDirection: "row",
justifyContent: "center"
},
card: {
height: "100%",
width: "100%",
padding: theme.spacing.unit * 2
},
root: {
width: "100%",
marginTop: theme.spacing.unit * 3
},
table: {
minWidth: 500
},
wrapper: {
flexGrow: 1,
display: "flex",
width: "100%"
},
tableWrapper: {
overflowX: "auto"
}
});
class ContactList extends React.Component {
componentWillMount = () => {
const { fetchContactList, meta } = this.props;
const { page = 0, size = 10 } = meta || {};
fetchContactList({ page, size });
};
handleChangePage = (event, page) => {
const { fetchContactList, meta = {} } = this.props;
fetchContactList({ page, size: meta.size });
};
handleChangeSize = event => {
const { fetchContactList, meta = {} } = this.props;
fetchContactList({ page: meta.page, size: event.target.value });
};
render() {
const { classes, isLoading, contacts, meta = {}, t } = this.props;
const emptyRows = 0;
if (isLoading) {
return (
<Screen>
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} />
</div>
</Screen>
);
}
return (
<Screen>
<div className={classes.wrapper}>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="textSecondary">
{t(`contact.listScreen.title`)}
</Typography>
<Typography variant="headline" component="h2">
{t(`contact.listScreen.headline`)}
</Typography>
</CardContent>
<CardContent>
<div className={classes.tableWrapper}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>{t("contact.label.firstname")}</TableCell>
<TableCell>{t("contact.label.lastname")}</TableCell>
<TableCell>{t("contact.label.email")}</TableCell>
<TableCell>{t("contact.label.phone")}</TableCell>
<TableCell>{t("contact.label.address")}</TableCell>
<TableCell>{t("contact.label.bankDetails")}</TableCell>
<TableCell>{t("contact.label.edit")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{contacts.map(contact => {
return (
<TableRow key={contact.id}>
<TableCell>{contact.firstName}</TableCell>
<TableCell>{contact.lastName}</TableCell>
<TableCell>{contact.email}</TableCell>
<TableCell>{contact.phone}</TableCell>
<TableCell>{contact.address}</TableCell>
<TableCell>{contact.bankDetails}</TableCell>
<TableCell>
<Link to={`/contact/${contact.id}/edit`}>
<CreateIcon />
</Link>
</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 48 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
colSpan={3}
count={meta.totalElements}
rowsPerPage={meta.size}
page={meta.page}
onChangePage={this.handleChangePage}
onChangeRowsPerPage={this.handleChangeSize}
Actions={ConnectedTablePaginationActionsWrapped}
/>
</TableRow>
</TableFooter>
</Table>
</div>
</CardContent>
</Card>
</div>
</Screen>
);
}
}
const mapStateToProps = (state, ownProps) => {
const contacts = getItems(apiMethod.list)(entity.contact)(state);
const meta = getMeta(apiMethod.list)(entity.contact)(state);
const isFetchingContacts = getIsFetching(apiMethod.list)(entity.contact)(
state
);
const timeFetchedContacts = getTimeFetched(apiMethod.list)(entity.contact)(
state
);
return {
isLoading: !timeFetchedContacts || isFetchingContacts,
isLoaded: !!timeFetchedContacts,
contacts,
meta
};
};
const mapDispatchToProps = {
fetchContactList: fetchList(entity.contact)
};
ContactList.propTypes = {
classes: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
const ConnectedContactList = connect(mapStateToProps, mapDispatchToProps)(
ContactList
);
export default translate()(withStyles(styles)(ConnectedContactList));

View File

@@ -0,0 +1,4 @@
import ContactListScreen from "./ContactListScreen";
import ContactDetailScreen from "./ContactDetailScreen";
export { ContactDetailScreen, ContactListScreen };

View File

@@ -0,0 +1,56 @@
import React from "react";
import PropTypes from "prop-types";
import { TextField, withStyles } from "material-ui";
import { Field } from "redux-form";
import i18n from "../../i18n";
export const formValidations = {
email: value =>
value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)
? i18n.t("validation.invalid_email_address")
: undefined,
required: value => (value ? undefined : i18n.t("validation.required")),
phone: value =>
value && !/^[0-9\- +]{4,15}$/i.test(value)
? i18n.t("validation.invalid_phone_number")
: undefined
};
const styles = theme => ({
formItem: {
margin: theme.spacing.unit,
width: "100%"
}
});
const renderTextField = ({
meta: { touched, error },
input: { value, ...inputRest },
...rest
}) => (
<TextField
{...rest}
{...inputRest}
value={value ? value : ""}
error={!!(touched && error)}
helperText={touched && error}
/>
);
const FormTextFieldBase = ({ label, name, classNames, classes, ...rest }) => (
<Field
label={label ? label : name}
name={name}
component={renderTextField}
className={classNames ? classNames : classes.formItem}
{...rest}
/>
);
FormTextFieldBase.propTypes = {
label: PropTypes.string,
name: PropTypes.string.isRequired,
classes: PropTypes.object.isRequired
};
export const FormTextField = withStyles(styles)(FormTextFieldBase);

View File

@@ -0,0 +1,282 @@
import React from "react";
import PropTypes from "prop-types";
import { withStyles } from "material-ui/styles";
import Table, {
TableHead,
TableBody,
TableCell,
TableFooter,
TablePagination,
TableRow
} from "material-ui/Table";
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 CreateIcon from "material-ui-icons/Create";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { Link } from "react-router-dom";
import { CircularProgress, Card, CardContent, Typography } from "material-ui";
import { apiMethod } from "../../config";
import { entity } from "../../lib/entity";
import {
getMeta,
getItems,
getIsFetching,
getTimeFetched
} from "../../redux/selectors";
import { fetchList } from "../../redux/actions";
import Screen from "../Screen";
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, this.props.totalPages - 1);
};
render() {
const { classes, page, theme, meta } = this.props;
return (
<div className={classes.root}>
<IconButton
onClick={this.handleFirstPageButtonClick}
disabled={page === 0}
aria-label="First Page"
>
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={this.handleBackButtonClick}
disabled={page === 0}
aria-label="Previous Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={this.handleNextButtonClick}
disabled={!meta.hasNext}
aria-label="Next Page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
<IconButton
onClick={this.handleLastPageButtonClick}
disabled={page === meta.totalPages - 1}
aria-label="Last Page"
>
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</div>
);
}
}
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 ConnectedTablePaginationActionsWrapped = connect(
state => ({ meta: getMeta(apiMethod.list)(entity.user)(state) }),
null
)(TablePaginationActionsWrapped);
const styles = theme => ({
progress: {
margin: theme.spacing.unit * 2
},
progressContainer: {
width: "100%",
flex: 1,
display: "flex",
flexDirection: "row",
justifyContent: "center"
},
card: {
height: "100%",
width: "100%",
padding: theme.spacing.unit * 2
},
root: {
width: "100%",
marginTop: theme.spacing.unit * 3
},
table: {
minWidth: 500
},
wrapper: {
flexGrow: 1,
display: "flex",
width: "100%"
},
tableWrapper: {
overflowX: "auto"
}
});
class UserList extends React.Component {
componentWillMount = () => {
const { fetchUserList, meta } = this.props;
const { page = 0, size = 10 } = meta || {};
fetchUserList({ page, size });
};
handleChangePage = (event, page) => {
const { fetchUserList, meta = {} } = this.props;
fetchUserList({ page, size: meta.size });
};
handleChangeSize = event => {
const { fetchUserList, meta = {} } = this.props;
fetchUserList({ page: meta.page, size: event.target.value });
};
render() {
const { classes, isLoading, users, meta = {}, t } = this.props;
const emptyRows = 0;
if (isLoading) {
return (
<Screen>
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} />
</div>
</Screen>
);
}
return (
<Screen>
<div className={classes.wrapper}>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="textSecondary">
{t(`user.listScreen.title`)}
</Typography>
<Typography variant="headline" component="h2">
{t(`user.listScreen.headline`)}
</Typography>
</CardContent>
<CardContent>
<div className={classes.tableWrapper}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>{t("user.label.id")}</TableCell>
<TableCell>{t("user.label.username")}</TableCell>
<TableCell>{t("user.label.edit")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map(user => {
return (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.username}</TableCell>
<TableCell>
<Link to={`/user/edit/${user.id}`}>
<CreateIcon />
</Link>
</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 48 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
colSpan={3}
count={meta.totalElements}
rowsPerPage={meta.size}
page={meta.page}
onChangePage={this.handleChangePage}
onChangeRowsPerPage={this.handleChangeSize}
Actions={ConnectedTablePaginationActionsWrapped}
/>
</TableRow>
</TableFooter>
</Table>
</div>
</CardContent>
</Card>
</div>
</Screen>
);
}
}
const mapStateToProps = (state, ownProps) => {
const users = getItems(apiMethod.list)(entity.user)(state);
const meta = getMeta(apiMethod.list)(entity.user)(state);
const isFetchingUsers = getIsFetching(apiMethod.list)(entity.user)(state);
const timeFetchedUsers = getTimeFetched(apiMethod.list)(entity.user)(state);
return {
isLoading: !timeFetchedUsers || isFetchingUsers,
isLoaded: !!timeFetchedUsers,
users,
meta
};
};
const mapDispatchToProps = {
fetchUserList: fetchList(entity.user)
};
UserList.propTypes = {
classes: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
const ConnectedUserList = connect(mapStateToProps, mapDispatchToProps)(
UserList
);
export default translate()(withStyles(styles)(ConnectedUserList));

View File

@@ -1,13 +1,21 @@
export const apiMethod = {
list: "list",
all: "all",
detail: "detail",
create: "create",
update: "update",
delete: "delete"
};
export const detailScreenType = {
view: "view",
edit: "edit",
create: "create"
};
export const apiHttpMethodMapping = {
list: "get",
all: "get",
detail: "get",
create: "post",
update: "put",
@@ -28,6 +36,9 @@ export const config = {
ERROR: {
NOCONNECTION: "NOCONNECTION",
UNAUTHORIZED: "UNAUTHORIZED"
},
CONSTANTS: {
FETCH_ALL_SIZE: 100
}
};

View File

@@ -3,6 +3,11 @@ export default {
hello: "Hello",
title: "Mitgliederverwaltung",
cancel: "Cancel",
validation: {
invalid_email_address: "Invalid Email Address",
required: "Required",
invalid_phone_number: "Invalid Phonenumber"
},
login: {
button_login: "Login",
label_username: "Email",
@@ -18,10 +23,54 @@ export default {
edit: "Edit",
list: "List"
},
contact: {
detailScreen: {
headline: {
create: "New Contact",
edit: "Contact {{name}}",
view: "Contact {{name}}"
},
saveButtonLabel: "Save",
title: {
create: "Create",
edit: "Edit"
}
},
listScreen: {
title: "List",
headline: "Contacts"
},
label: {
email: "Email",
firstname: "First Name",
lastname: "Last Name",
phone: "Phone",
address: "Address",
bankDetails: "Bank Details",
groups: "Groups",
edit: "Edit"
},
placeholder: {
groups: "Groups"
}
},
user_create_screen: {
create_user: "Create User"
},
user: {
listScreen: {
title: "List",
headline: "Users"
},
label: {
id: "ID",
username: "Email",
groups: "Groups",
edit: "Edit"
},
placeholder: {
groups: "Groups"
},
cancel: "Cancel",
delete: "Delete",
error_message: "Oops! Something went wrong",
@@ -41,6 +90,10 @@ export default {
checkBoxEnabled: "Enabled"
},
role: {
listScreen: {
title: "List",
headline: "Roles"
},
cancel: "Cancel",
delete: "Delete",
error_message: "Oops! Something went wrong",
@@ -75,6 +128,10 @@ export default {
delete: "Delete"
},
group: {
listScreen: {
title: "List",
headline: "Groups"
},
cancel: "Cancel",
delete: "Delete",
error_message: "Oops! Something went wrong",

View File

@@ -37,17 +37,18 @@ export const mergefetchRequest = action => state => {
};
export const mergefetchSuccess = action => state => {
const { method, payload = {} } = action;
const { data: { content, ...rest }, timeFetched } = payload;
const { data: { content, meta }, timeFetched } = payload;
const items = Array.isArray(content) ? content : [content];
const enhancedItems = items.map(item => ({ ...item, timeFetched }));
if (apiMethod[method]) {
return {
...state,
items: unionBy(items, state.items, "id"),
items: unionBy(enhancedItems, state.items, "id"),
[method]: {
...state[method],
isFetching: false,
items: items.map(item => item && item.id),
meta: Array.isArray(content) ? rest : null,
meta,
timeFetched
}
};

View File

@@ -1,4 +1,4 @@
import { apiActionType, apiMethod } from "../../config";
import { apiActionType, apiMethod, config } from "../../config";
//API Actions
export const CONNECTION_FAILURE = "CONNECTION_FAILURE";
@@ -23,15 +23,33 @@ export const fetchGeneric = method => entity => payload => ({
payload
});
export const FETCH_GENERIC_SUCCESS = FETCH_GENERIC(apiActionType.success);
export const fetchGenericSuccess = method => entity => data => ({
type: FETCH_GENERIC_SUCCESS,
method,
entity,
payload: {
timeFetched: new Date(),
data
}
});
export const fetchGenericSuccess = method => entity => data => {
const { content, pageable, ...rest } = data;
const meta = Array.isArray(content)
? {
size: pageable.pageSize,
page: pageable.pageNumber,
sort: pageable.sort,
hasNext: !rest.last,
first: rest.first,
totalElements: rest.totalElements,
totalPages: rest.totalPages
}
: null;
const transformedData = {
content: content ? content : data,
meta
};
return {
type: FETCH_GENERIC_SUCCESS,
method,
entity,
payload: {
timeFetched: new Date(),
data: transformedData
}
};
};
export const FETCH_GENERIC_FAILURE = FETCH_GENERIC(apiActionType.failure);
export const fetchGenericFailure = method => entity => error => ({
type: FETCH_GENERIC_FAILURE,
@@ -47,6 +65,13 @@ export const fetchList = entity => parameters =>
fetchGeneric(apiMethod.list)(entity)({ parameters });
export const fetchListSuccess = fetchGenericSuccess(apiMethod.list);
export const fetchListFailure = fetchGenericFailure(apiMethod.list);
export const fetchAll = entity => (page = 0) =>
fetchGeneric(apiMethod.all)(entity)({
parameters: {
size: config.CONSTANTS.FETCH_ALL_SIZE,
page
}
});
export const fetchDetail = fetchGeneric(apiMethod.detail);
export const fetchDetailById = entity => id =>
fetchGeneric(apiMethod.detail)(entity)({ data: { id } });

View File

@@ -10,7 +10,8 @@ import {
fetchLoginFailure,
reduxRehydrationCompleted,
fetchGenericSuccess,
fetchGenericFailure
fetchGenericFailure,
fetchAll
} from "../actions";
import {
LOGOUT,
@@ -77,6 +78,8 @@ export function* fetchSaga(api, action) {
};
if (method === apiMethod.list) {
// no todo yet
} else if (method === apiMethod.all) {
// no todo yet
} else if (method === apiMethod.detail) {
request.endpoint = `${entity.endpoint}/${data.id}`;
} else if (method === apiMethod.create) {
@@ -116,10 +119,16 @@ export function* fetchSaga(api, action) {
}
export function* fetchSuccessSaga(action) {
const { method, entity } = action;
const { method, entity, payload = {} } = action;
const { data: { meta } } = payload || {};
if (method === apiMethod.delete || method === apiMethod.create) {
yield put(push(`/${entity.name}/list`));
}
if (method === apiMethod.all) {
if (meta && meta.hasNext && (meta.page || meta.page === 0)) {
yield put(fetchAll(entity)(meta.page + 1));
}
}
}
export function* fetchLoginSuccessCallback(data) {

View File

@@ -8,7 +8,7 @@ import { entity } from "../../lib/entity";
import { apiMethod } from "../../config";
export const getContacts = getItems(apiMethod.list)(entity.contact);
export const getContactById = getItemById(apiMethod.list)(entity.contact);
export const getContactById = getItemById(entity.contact);
export const isFetchingContactList = getIsFetching(apiMethod.list)(
entity.contact
);

View File

@@ -18,7 +18,8 @@ export const getError = getEntityMethodValue("error");
export const getTimeFetched = getEntityMethodValue("timeFetched");
export const getIsFetching = getEntityMethodValue("isFetching");
export const getItemById = method => entity => id => state => {
const items = getItems(method)(entity)(state);
return find(items, item => item.id === id);
export const getItemById = entity => id => state => {
const parsedId = parseInt(id, 10);
const items = getEntityItems(entity)(state);
return find(items, item => item.id === parsedId);
};

View File

@@ -5,6 +5,7 @@ export const getFormValues = formName => state =>
export const getLoginFormValues = state => getFormValues("login")(state);
export const getUserFormValues = state =>
getFormValues("UserCreateForm")(state);
export const getContactFormValues = state => getFormValues("contact")(state);
export const getGroupFormValues = state =>
getFormValues("GroupCreateForm")(state);
export const getRoleFormValues = state =>

View File

@@ -4,7 +4,7 @@ import { entity } from "../../lib/entity";
import { getItems, getItemById } from "./entitySelectors";
export const getGroups = getItems(apiMethod.list)(entity.group);
export const getGroupById = getItemById(apiMethod.list)(entity.group);
export const getGroupById = getItemById(entity.group);
export const isLoadedGroups = state =>
!get(state, "group.list.isFetching", false) &&

View File

@@ -8,3 +8,4 @@ export * from "./permissionSelectors";
export * from "./roleSelectors";
export * from "./userSelectors";
export * from "./groupSelectors";
export * from "./contactsSelectors";

View File

@@ -1,2 +1,14 @@
import { get } from "lodash";
import { getItemById } from ".";
import { entity } from "../../lib/entity";
export const getLogin = (key = "isLoggedIn") => state => state.login[key];
export const getLoginError = state => getLogin("error")(state);
export const getUserIdFromToken = state => get(state, "login.decodedJwt.id");
export const getContactIdFromToken = state => {
const userId = getUserIdFromToken(state);
if (!userId) return undefined;
const user = getItemById(entity.user)(userId)(state);
return (user && user.contact) || undefined;
};

View File

@@ -4,7 +4,7 @@ import { entity } from "../../lib/entity";
import { getItems, getItemById } from "./entitySelectors";
export const getPermissions = getItems(apiMethod.list)(entity.permission);
export const getPermissionById = getItemById(apiMethod.list)(entity.permission);
export const getPermissionById = getItemById(entity.permission);
export const isFetchingPermissions = state =>
!!get(state, `${entity.permission.name}.list.isFetching`);

View File

@@ -4,7 +4,7 @@ import { entity } from "../../lib/entity";
import { getItems, getItemById } from "./entitySelectors";
export const getRoles = getItems(apiMethod.list)(entity.role);
export const getRoleById = getItemById(apiMethod.list)(entity.role);
export const getRoleById = getItemById(entity.role);
export const getCreateRole = state => get(state, "role.create");

View File

@@ -9,7 +9,7 @@ import { apiMethod } from "../../config";
import { get } from "lodash";
export const getUsers = getItems(apiMethod.list)(entity.user);
export const getUserById = getItemById(apiMethod.list)(entity.user);
export const getUserById = getItemById(entity.user);
export const isFetchingUserList = getIsFetching(apiMethod.list)(entity.user);
export const getTimeFetchedUserList = getTimeFetched(apiMethod.list)(
entity.user

View File

@@ -24,9 +24,9 @@
version "9.4.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.0.tgz#b85a0bcf1e1cc84eb4901b7e96966aedc6f078d1"
"@types/react-transition-group@^2.0.6":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.7.tgz#2847292d54c5685d982ae5a3ecb6960946689d87"
"@types/react-transition-group@^2.0.8":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.8.tgz#1ea86f6d8288e4bba8d743317ba9cc61cdacc1ad"
dependencies:
"@types/react" "*"
@@ -3453,6 +3453,10 @@ hoist-non-react-statics@2.3.1, hoist-non-react-statics@^2.2.1, hoist-non-react-s
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -5086,18 +5090,18 @@ material-ui-icons@^1.0.0-beta.17:
dependencies:
recompose "^0.26.0"
material-ui@^1.0.0-beta.35:
version "1.0.0-beta.35"
resolved "https://registry.yarnpkg.com/material-ui/-/material-ui-1.0.0-beta.35.tgz#eeac6be307c0469943c7686e5c6bd4eaa6b1b563"
material-ui@^1.0.0-beta.43:
version "1.0.0-beta.43"
resolved "https://registry.yarnpkg.com/material-ui/-/material-ui-1.0.0-beta.43.tgz#21074fd0ef5f1735a54060dbfd060e6d46fd5ef5"
dependencies:
"@types/jss" "^9.3.0"
"@types/react-transition-group" "^2.0.6"
"@types/react-transition-group" "^2.0.8"
babel-runtime "^6.26.0"
brcast "^3.0.1"
classnames "^2.2.5"
deepmerge "^2.0.1"
dom-helpers "^3.2.1"
hoist-non-react-statics "^2.3.1"
hoist-non-react-statics "^2.5.0"
jss "^9.3.3"
jss-camel-case "^6.0.0"
jss-default-unit "^8.0.2"
@@ -5111,7 +5115,8 @@ material-ui@^1.0.0-beta.35:
prop-types "^15.6.0"
react-event-listener "^0.5.1"
react-jss "^8.1.0"
react-popper "^0.8.0"
react-lifecycles-compat "^2.0.0"
react-popper "^0.10.0"
react-scrollbar-size "^2.0.2"
react-transition-group "^2.2.1"
recompose "^0.26.0"
@@ -5923,9 +5928,9 @@ pn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
popper.js@^1.12.9:
version "1.12.9"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.12.9.tgz#0dfbc2dff96c451bb332edcfcfaaf566d331d5b3"
popper.js@^1.14.1:
version "1.14.3"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
portfinder@^1.0.9:
version "1.0.13"
@@ -6304,6 +6309,14 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9,
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.3.1"
object-assign "^4.1.1"
proxy-addr@~2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
@@ -6510,18 +6523,22 @@ react-jss@^8.1.0:
prop-types "^15.6.0"
theming "^1.3.0"
react-lifecycles-compat@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-2.0.2.tgz#00a23160eec17a43b94dd74f95d44a1a2c3c5ec1"
react-maskinput@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/react-maskinput/-/react-maskinput-1.0.0.tgz#4d4e4c42cadd289c219256c34fd741ef552ba830"
dependencies:
input-core "^1.0.0"
react-popper@^0.8.0:
version "0.8.2"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.8.2.tgz#092095ff13933211d3856d9f325511ec3a42f12c"
react-popper@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.10.1.tgz#6a1f2595faffda77105bed4e89ecf22607a4c452"
dependencies:
popper.js "^1.12.9"
prop-types "^15.6.0"
popper.js "^1.14.1"
prop-types "^15.6.1"
react-reconciler@^0.7.0:
version "0.7.0"