initial copy from uni gitlab
This commit is contained in:
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -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
|
||||
|
||||
|
||||
32
src/App.css
Normal file
32
src/App.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
104
src/App.js
Normal file
104
src/App.js
Normal file
@@ -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 (
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<ConnectedRouter history={history}>
|
||||
<div>
|
||||
<Route exact path="/" component={LandingPage} />
|
||||
<Route exact path="/login" component={Login} />
|
||||
<Route exact path="/contact/list" component={ContactList} />
|
||||
<Route exact path="/user/list" component={UserList} />
|
||||
<Route
|
||||
exact
|
||||
path="/user/edit/:id"
|
||||
component={UserCreateScreen}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/user/create"
|
||||
component={UserCreateScreen}
|
||||
/>
|
||||
<Route exact path="/profile" component={UserCreateScreen} />
|
||||
<Route exact path="/role/list" component={RoleList} />
|
||||
<Route
|
||||
exact
|
||||
path="/role/edit/:id"
|
||||
component={RoleCreateScreen}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/role/create"
|
||||
component={RoleCreateScreen}
|
||||
/>
|
||||
<Route exact path="/group/list" component={GroupList} />
|
||||
<Route
|
||||
exact
|
||||
path="/group/edit/:id"
|
||||
component={GroupCreateScreen}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/group/create"
|
||||
component={GroupCreateScreen}
|
||||
/>
|
||||
</div>
|
||||
</ConnectedRouter>
|
||||
</MuiThemeProvider>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</PersistGate>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
8
src/App.test.js
Normal file
8
src/App.test.js
Normal file
@@ -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(<App />);
|
||||
});
|
||||
60
src/components/Checkbox.js
Normal file
60
src/components/Checkbox.js
Normal file
@@ -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 (
|
||||
<FormGroup row>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={event => onChange(name, event.target.checked)}
|
||||
value="checked"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CheckboxLabels.propTypes = {
|
||||
classes: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(CheckboxLabels);
|
||||
207
src/components/ContactList.js
Normal file
207
src/components/ContactList.js
Normal file
@@ -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 (
|
||||
<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);
|
||||
183
src/components/GroupCreateForm.js
Normal file
183
src/components/GroupCreateForm.js
Normal file
@@ -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 }) => (
|
||||
<TextField {...rest} {...input} error={touched && error} />
|
||||
);
|
||||
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} className={classes.userCreateForm}>
|
||||
<Paper className={classes.root} elevation={4}>
|
||||
<Typography type="headline" component="h2">
|
||||
{!!validID
|
||||
? t("group.edit_group_header")
|
||||
: t("group.create_group_header")}
|
||||
</Typography>
|
||||
<div className={classes.formRow}>
|
||||
<Field
|
||||
label={t("group.group_name")}
|
||||
name="name"
|
||||
component={renderTextField}
|
||||
className={classes.formItem}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectPermission"}
|
||||
options={permissionToChoose}
|
||||
handleChangeMulti={handleChangeSingle("singlePermission")}
|
||||
multi={singlePermission}
|
||||
canAddMultipleValues={false}
|
||||
placeholder={t("group.select_one_permission")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectUsers"}
|
||||
options={users}
|
||||
canAddMultipleValues={true}
|
||||
handleChangeMulti={handleChange("multiUsers")}
|
||||
multi={multiUsers}
|
||||
placeholder={t("group.select_users")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectContacts"}
|
||||
options={contacts}
|
||||
canAddMultipleValues={true}
|
||||
handleChangeMulti={handleChange("multiContacts")}
|
||||
multi={multiContacts}
|
||||
placeholder={t("group.select_contacts")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectResponsibles"}
|
||||
options={contacts}
|
||||
canAddMultipleValues={true}
|
||||
handleChangeMulti={handleChange("multiResponsibles")}
|
||||
multi={multiResponsibles}
|
||||
placeholder={t("group.select_responsibles")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classes.buttonsRow}>
|
||||
<div className={classes.buttonRow}>
|
||||
<Link className={classes.cancelButton} to="/group/list">
|
||||
<Button color="primary">{t("group.cancel")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{!!validID ? (
|
||||
<div className={classes.buttonRow}>
|
||||
<Button onClick={handleDelete} color="primary">
|
||||
{t("group.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={classes.buttonRow}>
|
||||
<Button color="primary" type="submit" disabled={!canSubmit}>
|
||||
{!!validID ? t("group.save_group") : t("group.add_group")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
);
|
||||
21
src/components/GroupCreateForm.test.js
Normal file
21
src/components/GroupCreateForm.test.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { GroupCreateForm } from "./GroupCreateForm";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
shallow(
|
||||
<GroupCreateForm
|
||||
t={jest.fn()}
|
||||
classes={{}}
|
||||
theme={{}}
|
||||
multi={{}}
|
||||
initialValues={{}}
|
||||
options={{}}
|
||||
handleChangeMulti={jest.fn}
|
||||
handleChangeSingle={jest.fn}
|
||||
handleChange={jest.fn}
|
||||
onSubmit={jest.fn}
|
||||
id={{}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
312
src/components/GroupCreateScreen.js
Normal file
312
src/components/GroupCreateScreen.js
Normal file
@@ -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 ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{t("group.group_is_edited_msg")}
|
||||
</div>
|
||||
) : null;
|
||||
} else {
|
||||
alert = isEdited ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{t("group.group_created_msg")}
|
||||
</div>
|
||||
) : 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 = (
|
||||
<Screen>
|
||||
<div className={classes.progressContainer}>
|
||||
<CircularProgress className={classes.progress} />
|
||||
</div>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
if (id) {
|
||||
if (!isLoaded) {
|
||||
return loadingScreen;
|
||||
}
|
||||
}
|
||||
|
||||
if (!!id && !isSent && !isLoaded) {
|
||||
return loadingScreen;
|
||||
}
|
||||
|
||||
const canSubmit = values && values.name ? true : false;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<GroupCreateForm
|
||||
initialValues={{ name: groupName, description: groupDescription }}
|
||||
multiUsers={multiUsers}
|
||||
multiContacts={multiContacts}
|
||||
multiResponsibles={multiResponsibles}
|
||||
singlePermission={singlePermission}
|
||||
handleChange={this.handleChange}
|
||||
handleChangeSingle={this.handleChangeSingle}
|
||||
users={mappedUsers}
|
||||
contacts={mappedContacts}
|
||||
permissionToChoose={mappedPermission}
|
||||
handleDelete={this.handleDelete}
|
||||
ref={form => (this.form = form)}
|
||||
onSubmit={this.handleSubmit}
|
||||
canSubmit={canSubmit}
|
||||
id={id}
|
||||
/>
|
||||
{alert}
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
open={isGroupWithErrors}
|
||||
onClose={this.handleClose}
|
||||
SnackbarContentProps={{
|
||||
"aria-describedby": "message-id"
|
||||
}}
|
||||
//TODO replace displayed text with the message from the error object.
|
||||
message={<span id="message-id">{t("group.error_message")}</span>}
|
||||
/>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
327
src/components/GroupList.js
Normal file
327
src/components/GroupList.js
Normal file
@@ -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 (
|
||||
<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={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="Next Page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowLeft />
|
||||
) : (
|
||||
<KeyboardArrowRight />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={this.handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 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 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 (
|
||||
<Screen>
|
||||
<Typography type="headline" component="h2" align="center">
|
||||
{t("group_list.header")}
|
||||
</Typography>
|
||||
<Paper className={classes.root}>
|
||||
<div className={classes.tableWrapper}>
|
||||
<Table className={classes.table} align="center">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("group_list.name")}</TableCell>
|
||||
<TableCell>{t("group_list.permission")}</TableCell>
|
||||
<TableCell style={{ width: 250 }}>
|
||||
{t("group_list.users")}
|
||||
</TableCell>
|
||||
<TableCell style={{ width: 250 }}>
|
||||
{t("group_list.contacts")}
|
||||
</TableCell>
|
||||
<TableCell style={{ width: 250 }}>
|
||||
{t("group_list.responsibles")}
|
||||
</TableCell>
|
||||
<TableCell>{t("group_list.edit")}</TableCell>
|
||||
<TableCell>{t("group_list.delete")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{newGroups
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map(group => {
|
||||
return (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell>{group.name}</TableCell>
|
||||
<TableCell>{group.permission}</TableCell>
|
||||
<TableCell>
|
||||
{group.users ? (
|
||||
<div>
|
||||
{group.users.map(user => {
|
||||
return <p key={user.id}>{user.username}</p>;
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{group.contacts ? (
|
||||
<div>
|
||||
{group.contacts.map(contact => {
|
||||
return (
|
||||
<p key={contact.id}>{`${contact.lastName}, ${
|
||||
contact.firstName
|
||||
}`}</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{group.responsibles ? (
|
||||
<div>
|
||||
{group.responsibles.map(responsibility => {
|
||||
return (
|
||||
<p key={responsibility.id}>{`${
|
||||
responsibility.lastName
|
||||
}, ${responsibility.firstName}`}</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`/group/edit/${group.id}`}>
|
||||
<CreateIcon />
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => this.handleDelete(group.id)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: 48 * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
colSpan={3}
|
||||
count={Object.keys(groups).length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onChangePage={this.handleChangePage}
|
||||
onChangeRowsPerPage={this.handleChangeRowsPerPage}
|
||||
Actions={TablePaginationActionsWrapped}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</Paper>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
17
src/components/LandingPage.js
Normal file
17
src/components/LandingPage.js
Normal file
@@ -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 <Screen />;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, { push: push })(LandingPage);
|
||||
92
src/components/Login.js
Normal file
92
src/components/Login.js
Normal file
@@ -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 (
|
||||
<div>
|
||||
<LoginForm
|
||||
ref={form => (this.form = form)}
|
||||
initialValues={{ username: "", password: "" }}
|
||||
onSubmit={this.submit}
|
||||
canSubmit={canSubmit}
|
||||
/>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
open={!!error}
|
||||
onClose={this.handleClose}
|
||||
SnackbarContentProps={{
|
||||
"aria-describedby": "message-id"
|
||||
}}
|
||||
//TODO replace displayed tex with the message from the error object.
|
||||
message={<span id="message-id">Failed to Login</span>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
16
src/components/Login.test.js
Normal file
16
src/components/Login.test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { Login } from "./Login";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
shallow(<Login t={jest.fn()} />);
|
||||
});
|
||||
|
||||
it("has exactly one LoginForm", () => {
|
||||
const component = shallow(<Login t={jest.fn()} />);
|
||||
const initialValues = component
|
||||
.find("ReduxForm")
|
||||
.map(form => form.prop("initialValues"));
|
||||
expect(initialValues).toHaveLength(1);
|
||||
expect(initialValues[0]).toEqual({ username: "", password: "" });
|
||||
});
|
||||
77
src/components/LoginForm.js
Normal file
77
src/components/LoginForm.js
Normal file
@@ -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 }) => (
|
||||
<TextField {...rest} {...input} error={touched && error} />
|
||||
);
|
||||
|
||||
export class LoginForm extends Component {
|
||||
render() {
|
||||
const { handleSubmit, canSubmit, classes, t } = this.props;
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classes.loginForm}>
|
||||
<div className={classes.formRow}>
|
||||
<Field
|
||||
label={t("login.label_username")}
|
||||
name="username"
|
||||
component={renderTextField}
|
||||
className={classes.formItem}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<Field
|
||||
label={t("login.label_password")}
|
||||
name="password"
|
||||
type="password"
|
||||
component={renderTextField}
|
||||
className={classes.formItem}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<Button
|
||||
raised
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
className={classes.formItem}
|
||||
>
|
||||
{t("login.button_login")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginForm.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default reduxForm({ form: "login" })(
|
||||
withStyles(styles)(translate()(LoginForm))
|
||||
);
|
||||
118
src/components/MenuAppBar.js
Normal file
118
src/components/MenuAppBar.js
Normal file
@@ -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 (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
className={classes.menuButton}
|
||||
color="inherit"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography type="title" color="inherit" className={classes.flex}>
|
||||
Mitgliederverwaltung
|
||||
</Typography>
|
||||
{auth && (
|
||||
<div>
|
||||
<IconButton
|
||||
aria-owns={open ? "menu-appbar" : null}
|
||||
aria-haspopup="true"
|
||||
onClick={this.handleMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right"
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right"
|
||||
}}
|
||||
open={open}
|
||||
onClose={this.handleClose}
|
||||
>
|
||||
<MenuItem onClick={this.handleClose}>Profile</MenuItem>
|
||||
<MenuItem onClick={this.handleLogout}>Logout</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MenuAppBar.propTypes = {
|
||||
classes: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
logout
|
||||
};
|
||||
|
||||
const ConnectedMenuAppBar = connect(mapStateToProps, mapDispatchToProps)(
|
||||
MenuAppBar
|
||||
);
|
||||
|
||||
export default withStyles(styles)(ConnectedMenuAppBar);
|
||||
242
src/components/ReactSelect.js
Normal file
242
src/components/ReactSelect.js
Normal file
@@ -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 (
|
||||
<MenuItem
|
||||
onFocus={onFocus}
|
||||
selected={isFocused}
|
||||
onClick={this.handleClick}
|
||||
component="div"
|
||||
style={{
|
||||
fontWeight: isSelected ? 500 : 400
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function SelectWrapped(props) {
|
||||
const { classes, ...other } = props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
optionComponent={Option}
|
||||
noResultsText={<Typography>{"No results found"}</Typography>}
|
||||
arrowRenderer={arrowProps => {
|
||||
return arrowProps.isOpen ? <ArrowDropUpIcon /> : <ArrowDropDownIcon />;
|
||||
}}
|
||||
clearRenderer={() => <ClearIcon />}
|
||||
valueComponent={valueProps => {
|
||||
const { value, children, onRemove } = valueProps;
|
||||
|
||||
const onDelete = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onRemove(value);
|
||||
};
|
||||
|
||||
if (onRemove) {
|
||||
return (
|
||||
<Chip
|
||||
tabIndex={-1}
|
||||
label={children}
|
||||
className={classes.chip}
|
||||
deleteIcon={<CancelIcon onTouchEnd={onDelete} />}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="Select-value">{children}</div>;
|
||||
}}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The class handling used multi-values should have and give to React-Select following lines of code:
|
||||
* state = {
|
||||
multi: null
|
||||
};
|
||||
|
||||
handleChangeMulti = multi => {
|
||||
this.setState({
|
||||
multi
|
||||
});
|
||||
};
|
||||
The selected values will then be available in this.state.multi
|
||||
*/
|
||||
export class ReactSelect extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
name,
|
||||
options,
|
||||
placeholder,
|
||||
handleChangeMulti,
|
||||
multi,
|
||||
canAddMultipleValues
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Input
|
||||
fullWidth
|
||||
inputComponent={SelectWrapped}
|
||||
name={name}
|
||||
inputProps={{
|
||||
classes,
|
||||
value: multi,
|
||||
multi: canAddMultipleValues,
|
||||
onChange: handleChangeMulti,
|
||||
placeholder: placeholder ? placeholder : "Select multi-value...",
|
||||
instanceId: "react-select-chip",
|
||||
id: "react-select-chip",
|
||||
name: "react-select-chip",
|
||||
simpleValue: true,
|
||||
options
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactSelect.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
multi: PropTypes.object,
|
||||
options: PropTypes.array.isRequired,
|
||||
handleChangeMulti: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(translate()(ReactSelect));
|
||||
153
src/components/RoleCreateForm.js
Normal file
153
src/components/RoleCreateForm.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router-dom";
|
||||
import { withStyles } from "material-ui/styles";
|
||||
import Typography from "material-ui/Typography";
|
||||
import Paper from "material-ui/Paper";
|
||||
import { Field, reduxForm } from "redux-form";
|
||||
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"
|
||||
},
|
||||
formItem: {
|
||||
margin: theme.spacing.unit,
|
||||
width: 700
|
||||
},
|
||||
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 }) => (
|
||||
<TextField {...rest} {...input} error={touched && error} />
|
||||
);
|
||||
|
||||
export class RoleCreateForm extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
optionsPermissions,
|
||||
optionsUsers,
|
||||
handleDelete,
|
||||
handleSubmit,
|
||||
handleChangeMultiPermissions,
|
||||
multiPermissions,
|
||||
handleChangeMultiUsers,
|
||||
multiUsers,
|
||||
initialValues,
|
||||
canSubmit,
|
||||
id,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classes.userCreateForm}>
|
||||
<Paper className={classes.root} elevation={4}>
|
||||
<Typography type="headline" component="h2">
|
||||
{!!id || id === 0
|
||||
? t("role.edit_role_header")
|
||||
: t("role.create_role_header")}
|
||||
</Typography>
|
||||
<div className={classes.formRow}>
|
||||
<Field
|
||||
label={t("role.role_name")}
|
||||
name="name"
|
||||
initialValues={initialValues.name}
|
||||
component={renderTextField}
|
||||
className={classes.formItem}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectPermissions"}
|
||||
options={optionsPermissions}
|
||||
handleChangeMulti={handleChangeMultiPermissions}
|
||||
multi={multiPermissions}
|
||||
canAddMultipleValues={true}
|
||||
placeholder={t("role.select_permissions")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectUsers"}
|
||||
options={optionsUsers}
|
||||
handleChangeMulti={handleChangeMultiUsers}
|
||||
multi={multiUsers}
|
||||
canAddMultipleValues={true}
|
||||
placeholder={t("role.select_users")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classes.buttonsRow}>
|
||||
<div className={classes.buttonRow}>
|
||||
<Link className={classes.cancelButton} to="/role/list">
|
||||
<Button color="primary">{t("role.cancel")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{!!id || id === 0 ? (
|
||||
<div className={classes.buttonRow}>
|
||||
<Button onClick={handleDelete} color="primary">
|
||||
{t("role.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={classes.buttonRow}>
|
||||
<Button color="primary" type="submit" disabled={!canSubmit}>
|
||||
{!!id || id === 0 ? t("role.save_role") : t("role.add_role")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RoleCreateForm.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default reduxForm({ form: "RoleCreateForm", enableReinitialize: true })(
|
||||
withStyles(styles)(translate()(RoleCreateForm))
|
||||
);
|
||||
19
src/components/RoleCreateForm.test.js
Normal file
19
src/components/RoleCreateForm.test.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { RoleCreateForm } from "./RoleCreateForm";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
shallow(
|
||||
<RoleCreateForm
|
||||
t={jest.fn()}
|
||||
classes={{}}
|
||||
theme={{}}
|
||||
multi={{}}
|
||||
initialValues={{}}
|
||||
options={{}}
|
||||
handleChangeMulti={jest.fn}
|
||||
onSubmit={jest.fn}
|
||||
id={{}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
275
src/components/RoleCreateScreen.js
Normal file
275
src/components/RoleCreateScreen.js
Normal file
@@ -0,0 +1,275 @@
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { CircularProgress, withStyles, Snackbar } from "material-ui";
|
||||
|
||||
import RoleCreateForm from "./RoleCreateForm";
|
||||
import {
|
||||
fetchCreateByObject,
|
||||
fetchDeleteById,
|
||||
fetchUpdateByObject,
|
||||
fetchDetailById,
|
||||
fetchList,
|
||||
resetRoleCreateState
|
||||
} from "../redux/actions";
|
||||
import {
|
||||
getPermissions,
|
||||
getUsers,
|
||||
getRoleFormValues,
|
||||
getTimeFetchedPermissions,
|
||||
isFetchingPermissions,
|
||||
isCreatingRole,
|
||||
getCreateRoleTimeFetched,
|
||||
getCreateRoleError,
|
||||
isEditedRole,
|
||||
isLoadedRole,
|
||||
getRoleById,
|
||||
isLoadedRoles,
|
||||
isRoleWithErrors
|
||||
} from "../redux/selectors";
|
||||
import Screen from "./Screen";
|
||||
import { entity } from "../lib/entity";
|
||||
|
||||
const styles = theme => ({
|
||||
progress: {
|
||||
margin: theme.spacing.unit * 2
|
||||
},
|
||||
progressContainer: {
|
||||
width: "100%",
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center"
|
||||
}
|
||||
});
|
||||
|
||||
export class RoleCreateScreen extends Component {
|
||||
state = {
|
||||
multiPermissions: [],
|
||||
multiUsers: [],
|
||||
roleName: "",
|
||||
setFormValues: false
|
||||
};
|
||||
|
||||
handleChangeMultiPermissions = multiPermissions => {
|
||||
this.setState({
|
||||
multiPermissions: !!multiPermissions
|
||||
? multiPermissions.split(",").map(v => parseInt(v, 10))
|
||||
: []
|
||||
});
|
||||
};
|
||||
|
||||
handleChangeMultiUsers = multiUsers => {
|
||||
this.setState({
|
||||
multiUsers: !!multiUsers
|
||||
? multiUsers.split(",").map(v => parseInt(v, 10))
|
||||
: []
|
||||
});
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
const {
|
||||
fetchPermissions,
|
||||
fetchUsers,
|
||||
fetchRoles,
|
||||
fetchRole,
|
||||
id
|
||||
} = this.props;
|
||||
fetchRoles({});
|
||||
fetchPermissions({});
|
||||
fetchUsers({});
|
||||
if (!!id || id === 0) {
|
||||
fetchRole(id);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.setState({
|
||||
multiPermissions: [],
|
||||
multiUsers: [],
|
||||
roleName: "",
|
||||
setFormValues: false
|
||||
});
|
||||
const { resetRoleCreateState } = this.props;
|
||||
resetRoleCreateState();
|
||||
};
|
||||
|
||||
componentWillReceiveProps = (props, state) => {
|
||||
const { id, roleForEdit, isLoadedRole, isLoadedRoles } = props;
|
||||
const { setFormValues } = this.state;
|
||||
|
||||
if (
|
||||
(parseInt(id, 10) || parseInt(id, 10) === 0) &&
|
||||
!!isLoadedRole &&
|
||||
!!isLoadedRoles &&
|
||||
!setFormValues
|
||||
) {
|
||||
this.setState({
|
||||
multiPermissions: roleForEdit.permissions || [],
|
||||
multiUsers: roleForEdit.users || [],
|
||||
roleName: roleForEdit.name || "",
|
||||
setFormValues: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleDelete = () => {
|
||||
const { fetchDeleteRole, id } = this.props;
|
||||
fetchDeleteRole(id);
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
const { fetchCreateRole, fetchEditRole, values: { name }, id } = this.props;
|
||||
const { multiPermissions, multiUsers } = this.state;
|
||||
!!id || id === 0
|
||||
? fetchEditRole({
|
||||
id,
|
||||
name,
|
||||
permissions: multiPermissions,
|
||||
users: multiUsers
|
||||
})
|
||||
: fetchCreateRole({
|
||||
name,
|
||||
permissions: multiPermissions,
|
||||
users: multiUsers
|
||||
});
|
||||
};
|
||||
|
||||
//todo: add canSubmit
|
||||
//todo: HANDLE trying to view unexisting role
|
||||
render() {
|
||||
const {
|
||||
permissions,
|
||||
users,
|
||||
isRoleWithErrors,
|
||||
isLoaded,
|
||||
classes,
|
||||
isEdited,
|
||||
isSent,
|
||||
values,
|
||||
id,
|
||||
t
|
||||
} = this.props;
|
||||
const { roleName } = this.state;
|
||||
const optionsPermissions = (permissions || []).map(permission => {
|
||||
return { value: permission.id, label: permission.name };
|
||||
});
|
||||
const optionsUsers = (users || []).map(user => {
|
||||
return { value: user.id, label: user.username };
|
||||
});
|
||||
|
||||
var alert = null;
|
||||
if (!!id || id === 0) {
|
||||
alert = isEdited ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{t("role.role_is_edited_msg")}
|
||||
</div>
|
||||
) : null;
|
||||
} else {
|
||||
alert = isEdited ? (
|
||||
<div style={{ textAlign: "center" }}>{t("role.role_created_msg")}</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const loadingScreen = (
|
||||
<Screen>
|
||||
<div className={classes.progressContainer}>
|
||||
<CircularProgress className={classes.progress} />
|
||||
</div>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
/*
|
||||
if (!!id || id===0) {
|
||||
if (!isLoaded) {
|
||||
return loadingScreen;
|
||||
}
|
||||
}
|
||||
*/
|
||||
if ((!!id || id === 0) && !isSent && !isLoaded) {
|
||||
return loadingScreen;
|
||||
}
|
||||
|
||||
const canSubmit = values && values.name ? true : false;
|
||||
return (
|
||||
<Screen>
|
||||
<RoleCreateForm
|
||||
multiPermissions={this.state.multiPermissions}
|
||||
multiUsers={this.state.multiUsers}
|
||||
initialValues={{ name: roleName }}
|
||||
optionsPermissions={optionsPermissions}
|
||||
optionsUsers={optionsUsers}
|
||||
handleChangeMultiPermissions={this.handleChangeMultiPermissions}
|
||||
handleChangeMultiUsers={this.handleChangeMultiUsers}
|
||||
ref={form => (this.form = form)}
|
||||
handleDelete={this.handleDelete}
|
||||
onSubmit={this.handleSubmit}
|
||||
canSubmit={canSubmit}
|
||||
id={id}
|
||||
/>
|
||||
{alert}
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
open={isRoleWithErrors}
|
||||
onClose={this.handleClose}
|
||||
SnackbarContentProps={{
|
||||
"aria-describedby": "message-id"
|
||||
}}
|
||||
//TODO replace displayed text with the message from the error object.
|
||||
message={<span id="message-id">{t("role.error_message")}</span>}
|
||||
/>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const id = parseInt(ownProps.match.params.id, 10);
|
||||
|
||||
const isFetchingPerms = isFetchingPermissions(state);
|
||||
const timeFetchedPermissions = getTimeFetchedPermissions(state);
|
||||
const isCreating = isCreatingRole(state);
|
||||
const isEdited = isEditedRole(state);
|
||||
const roleCreatedTime = getCreateRoleTimeFetched(state);
|
||||
const createRoleError = getCreateRoleError(state);
|
||||
|
||||
return {
|
||||
permissions: getPermissions(state),
|
||||
users: getUsers(state),
|
||||
role: state.role.create,
|
||||
roleForEdit: getRoleById(id)(state),
|
||||
values: getRoleFormValues(state),
|
||||
isFetchingPerms,
|
||||
isLoaded: !isFetchingPerms && !!timeFetchedPermissions,
|
||||
isLoadedRoles: isLoadedRoles(state),
|
||||
isLoadedRole: isLoadedRole(state),
|
||||
isSent: !isCreating && !!roleCreatedTime && !createRoleError,
|
||||
id,
|
||||
isEdited,
|
||||
isRoleWithErrors: isRoleWithErrors(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchPermissions: fetchList(entity.permission),
|
||||
fetchUsers: fetchList(entity.user),
|
||||
fetchCreateRole: fetchCreateByObject(entity.role),
|
||||
fetchRoles: fetchList(entity.role),
|
||||
fetchRole: fetchDetailById(entity.role),
|
||||
fetchEditRole: fetchUpdateByObject(entity.role),
|
||||
fetchDeleteRole: fetchDeleteById(entity.role),
|
||||
resetRoleCreateState
|
||||
};
|
||||
|
||||
export const ConnectedCreateScreen = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(RoleCreateScreen);
|
||||
|
||||
export default withStyles(styles)(translate()(ConnectedCreateScreen));
|
||||
328
src/components/RoleList.js
Normal file
328
src/components/RoleList.js
Normal file
@@ -0,0 +1,328 @@
|
||||
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 {
|
||||
getPermissions,
|
||||
getUsers,
|
||||
getRoleById,
|
||||
getRoles,
|
||||
getTimeFetchedPermissions,
|
||||
getTimeFetchedUserList
|
||||
} from "../redux/selectors";
|
||||
import { fetchDeleteById, fetchList } from "../redux/actions";
|
||||
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";
|
||||
|
||||
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)
|
||||
);
|
||||
};
|
||||
|
||||
//TODO FIX ADD ROLE
|
||||
render() {
|
||||
const { classes, count, page, rowsPerPage, theme } = 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={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="Next Page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowLeft />
|
||||
) : (
|
||||
<KeyboardArrowRight />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={this.handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 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 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 RoleList extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
rowsPerPage: 5
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
const { fetchRoles, fetchPermissions, fetchUsers } = this.props;
|
||||
fetchRoles();
|
||||
fetchPermissions();
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
handleChangePage = (event, page) => {
|
||||
this.setState({ page });
|
||||
};
|
||||
|
||||
handleChangeRowsPerPage = event => {
|
||||
this.setState({ rowsPerPage: event.target.value });
|
||||
};
|
||||
|
||||
handleDelete = id => {
|
||||
const { fetchDeleteRole, fetchRoles } = this.props;
|
||||
fetchDeleteRole(id);
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, roles, permissions, users, t, isLoading } = this.props;
|
||||
const { rowsPerPage, page } = this.state;
|
||||
const emptyRows =
|
||||
rowsPerPage -
|
||||
Math.min(rowsPerPage, (roles || []).length - page * rowsPerPage);
|
||||
|
||||
const newRoles = roles.map(role =>
|
||||
Object.assign({}, role, {
|
||||
users: !!role.users ? users.filter(u => role.users.includes(u.id)) : [],
|
||||
permissions: !!role.permissions
|
||||
? permissions.filter(p => role.permissions.includes(p.id))
|
||||
: []
|
||||
})
|
||||
);
|
||||
|
||||
const loadingScreen = (
|
||||
<Screen>
|
||||
<div className={classes.progressContainer}>
|
||||
<CircularProgress className={classes.progress} />
|
||||
</div>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return loadingScreen;
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Typography type="headline" component="h2" align="center">
|
||||
{t("role_list.header")}
|
||||
</Typography>
|
||||
<Paper className={classes.root}>
|
||||
<div className={classes.tableWrapper}>
|
||||
<Table className={classes.table} align="center">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("role_list.name")}</TableCell>
|
||||
<TableCell style={{ width: 250 }}>
|
||||
{t("role_list.permissions")}
|
||||
</TableCell>
|
||||
<TableCell style={{ width: 250 }}>
|
||||
{t("role_list.users")}
|
||||
</TableCell>
|
||||
<TableCell>{t("role_list.edit")}</TableCell>
|
||||
<TableCell>{t("role_list.delete")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(newRoles || [])
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map(role => {
|
||||
return (
|
||||
<TableRow key={role.id}>
|
||||
<TableCell>{role.name}</TableCell>
|
||||
<TableCell>
|
||||
{role.permissions ? (
|
||||
<div>
|
||||
{role.permissions.map(permission => {
|
||||
return (
|
||||
<p key={permission.id}>{permission.name}</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{role.users ? (
|
||||
<div>
|
||||
{role.users.map(user => {
|
||||
return <p key={user.id}>{user.username}</p>;
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`/role/edit/${role.id}`}>
|
||||
<CreateIcon />
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => this.handleDelete(role.id)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: 48 * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
colSpan={3}
|
||||
count={(roles || []).length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onChangePage={this.handleChangePage}
|
||||
onChangeRowsPerPage={this.handleChangeRowsPerPage}
|
||||
Actions={TablePaginationActionsWrapped}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</Paper>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const timeFetchedPermissions = getTimeFetchedPermissions(state);
|
||||
const timeFetchedUserList = getTimeFetchedUserList(state);
|
||||
return {
|
||||
roles: getRoles(state),
|
||||
role: id => getRoleById(id)(state),
|
||||
permissions: getPermissions(state),
|
||||
users: getUsers(state),
|
||||
timeFetchedPermissions,
|
||||
timeFetchedUserList,
|
||||
isLoading:
|
||||
!getRoles(state) || !timeFetchedPermissions || !timeFetchedUserList
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchRoles: fetchList(entity.role),
|
||||
fetchPermissions: fetchList(entity.permission),
|
||||
fetchUsers: fetchList(entity.user),
|
||||
fetchDeleteRole: fetchDeleteById(entity.role)
|
||||
};
|
||||
|
||||
RoleList.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export const ConnectedRoleList = connect(mapStateToProps, mapDispatchToProps)(
|
||||
RoleList
|
||||
);
|
||||
|
||||
export default withStyles(styles)(translate()(ConnectedRoleList));
|
||||
263
src/components/Screen.js
Normal file
263
src/components/Screen.js
Normal file
@@ -0,0 +1,263 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
withStyles,
|
||||
Drawer,
|
||||
IconButton,
|
||||
Divider,
|
||||
AppBar,
|
||||
List,
|
||||
ListItem,
|
||||
ListSubheader,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from "material-ui";
|
||||
import ChevronLeftIcon from "material-ui-icons/ChevronLeft";
|
||||
import ChevronRightIcon from "material-ui-icons/ChevronRight";
|
||||
import AddIcon from "material-ui-icons/Add";
|
||||
import ListIcon from "material-ui-icons/List";
|
||||
import { Link } from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
import { openDrawer, closeDrawer } from "../redux/actions";
|
||||
import { isDrawerOpen } from "../redux/selectors";
|
||||
import { translate } from "react-i18next";
|
||||
import Toolbar from "./Toolbar";
|
||||
|
||||
const styles = theme => {
|
||||
const { custom: { drawer } } = theme;
|
||||
const drawerWidth = drawer.width;
|
||||
const closedDrawerWidth = drawer.closedWidth;
|
||||
return {
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
minHeight: "100vh",
|
||||
zIndex: 1,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
display: "flex"
|
||||
},
|
||||
appBar: {
|
||||
position: "fixed",
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
})
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
})
|
||||
},
|
||||
menuButton: {
|
||||
marginLeft: 12,
|
||||
marginRight: 36
|
||||
},
|
||||
hide: {
|
||||
display: "none"
|
||||
},
|
||||
drawerPaper: {
|
||||
position: "fixed",
|
||||
backgroundColor: "white",
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
})
|
||||
},
|
||||
drawerPaperClose: {
|
||||
position: "fixed",
|
||||
backgroundColor: "white",
|
||||
width: closedDrawerWidth,
|
||||
overflowX: "hidden",
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
})
|
||||
},
|
||||
fullWidth: {
|
||||
width: "100%",
|
||||
marginRight: 0,
|
||||
marginLeft: 0,
|
||||
alignSelf: "center"
|
||||
},
|
||||
toolbar: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
padding: "0 8px",
|
||||
...theme.mixins.toolbar
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
padding: theme.spacing.unit * 3
|
||||
},
|
||||
noTextDecoration: {
|
||||
textDecoration: "none"
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const DrawerItem = ({
|
||||
icon = null,
|
||||
label = "",
|
||||
open = true,
|
||||
classes
|
||||
}) => (
|
||||
<ListItem button>
|
||||
{icon ? (
|
||||
<ListItemIcon className={open ? undefined : classes.fullWidth}>
|
||||
{icon}
|
||||
</ListItemIcon>
|
||||
) : null}
|
||||
{open ? <ListItemText primary={label} /> : null}
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
DrawerItem.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
open: PropTypes.bool
|
||||
};
|
||||
|
||||
export const DrawerListBase = props => {
|
||||
const { translate = true, header, items = [], t, classes, open } = props;
|
||||
return (
|
||||
<List>
|
||||
{header ? (
|
||||
<ListSubheader>
|
||||
{translate ? t(header.label) : header.label}
|
||||
</ListSubheader>
|
||||
) : null}
|
||||
{items.map((item, index) => {
|
||||
const { label, link, ...rest } = item;
|
||||
return (
|
||||
<Link className={classes.noTextDecoration} key={index} to={link}>
|
||||
<DrawerItem
|
||||
classes={classes}
|
||||
open={open}
|
||||
label={translate ? t(label) : label}
|
||||
{...rest}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
DrawerListBase.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
translate: PropTypes.bool,
|
||||
header: PropTypes.object,
|
||||
items: PropTypes.array,
|
||||
open: PropTypes.bool
|
||||
};
|
||||
|
||||
const DrawerList = translate()(withStyles(styles)(DrawerListBase));
|
||||
|
||||
const contactSection = [
|
||||
{ link: "/contact/list", label: "navigation.list", icon: <ListIcon /> },
|
||||
{ link: "/contact/create", label: "navigation.create", icon: <AddIcon /> }
|
||||
];
|
||||
const userSection = [
|
||||
{ link: "/user/list", label: "navigation.list", icon: <ListIcon /> },
|
||||
{ link: "/user/create", label: "navigation.create", icon: <AddIcon /> }
|
||||
];
|
||||
const roleSection = [
|
||||
{ link: "/role/list", label: "navigation.list", icon: <ListIcon /> },
|
||||
{ link: "/role/create", label: "navigation.create", icon: <AddIcon /> }
|
||||
];
|
||||
const groupSection = [
|
||||
{ link: "/group/list", label: "navigation.list", icon: <ListIcon /> },
|
||||
{ link: "/group/create", label: "navigation.create", icon: <AddIcon /> }
|
||||
];
|
||||
|
||||
export class Screen extends React.Component {
|
||||
render() {
|
||||
const { classes, theme, children, closeDrawer, open } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar
|
||||
position="absolute"
|
||||
className={classNames(classes.appBar, open && classes.appBarShift)}
|
||||
>
|
||||
<Toolbar />
|
||||
</AppBar>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
classes={{
|
||||
paper: classNames(
|
||||
classes.drawerPaper,
|
||||
!open && classes.drawerPaperClose
|
||||
)
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
<div className={classes.toolbar}>
|
||||
<IconButton onClick={closeDrawer}>
|
||||
{theme.direction === "rtl" ? (
|
||||
<ChevronRightIcon />
|
||||
) : (
|
||||
<ChevronLeftIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
<Divider />
|
||||
<DrawerList
|
||||
header={{ label: "navigation.header.contact" }}
|
||||
items={contactSection}
|
||||
open={open}
|
||||
/>
|
||||
<DrawerList
|
||||
header={{ label: "navigation.header.user" }}
|
||||
items={userSection}
|
||||
open={open}
|
||||
/>
|
||||
<DrawerList
|
||||
header={{ label: "Role" }}
|
||||
items={roleSection}
|
||||
open={open}
|
||||
/>
|
||||
<DrawerList
|
||||
header={{ label: "navigation.header.group" }}
|
||||
items={groupSection}
|
||||
open={open}
|
||||
/>
|
||||
</Drawer>
|
||||
<main className={classes.content}>
|
||||
<div className={classes.toolbar} />
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Screen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
open: isDrawerOpen(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openDrawer,
|
||||
closeDrawer
|
||||
};
|
||||
|
||||
const ConnectedScreen = connect(mapStateToProps, mapDispatchToProps)(
|
||||
withStyles(styles, { withTheme: true })(translate()(Screen))
|
||||
);
|
||||
|
||||
export default ConnectedScreen;
|
||||
91
src/components/Screen.test.js
Normal file
91
src/components/Screen.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { Screen, DrawerItem, DrawerListBase } from "./Screen";
|
||||
import CreateIcon from "material-ui-icons/Create";
|
||||
import AddIcon from "material-ui-icons/Add";
|
||||
import ListIcon from "material-ui-icons/List";
|
||||
import { ListItem, ListItemText, ListItemIcon } from "material-ui";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
shallow(<Screen t={jest.fn()} classes={{}} theme={{}} />);
|
||||
});
|
||||
|
||||
it("renders an AppBar", () => {
|
||||
const component = shallow(<Screen t={jest.fn()} classes={{}} theme={{}} />);
|
||||
const Appbar = component.find("WithStyles(AppBar)");
|
||||
expect(Appbar).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders a useful Drawer with DrawerLists", () => {
|
||||
const component = shallow(<Screen t={jest.fn()} classes={{}} theme={{}} />);
|
||||
const Drawer = component.find("WithStyles(Drawer)");
|
||||
expect(Drawer).toHaveLength(1);
|
||||
const DrawerLists = component.find("Translate(WithStyles(DrawerListBase))");
|
||||
expect(DrawerLists.length).toBeGreaterThan(1);
|
||||
const items = DrawerLists.map(list => list.prop("items"));
|
||||
items.forEach(item =>
|
||||
item.forEach(item => {
|
||||
expect(item).toHaveProperty("icon");
|
||||
expect(item).toHaveProperty("label");
|
||||
expect(item).toHaveProperty("link");
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("renders a DrawerItem without crashing", () => {
|
||||
shallow(<DrawerItem classes={{}} />);
|
||||
});
|
||||
|
||||
it("DrawerItem renders a ListItem", () => {
|
||||
const component = shallow(<DrawerItem classes={{}} />);
|
||||
const ListItemText = component.find("WithStyles(ListItemText)");
|
||||
expect(ListItemText).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("DrawerItem renders a ListItem with icon and without labels if drawer is closed", () => {
|
||||
const component = shallow(
|
||||
<DrawerItem icon={AddIcon} classes={{}} open={false} />
|
||||
);
|
||||
const ListItemText = component.find("WithStyles(ListItemText)");
|
||||
const ListItemIcon = component.find("WithStyles(ListItemIcon)");
|
||||
expect(ListItemText).toHaveLength(0);
|
||||
expect(ListItemIcon).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("DrawerItem renders a ListItem with icon and labels if drawer is open", () => {
|
||||
const component = shallow(
|
||||
<DrawerItem icon={AddIcon} classes={{}} open={true} />
|
||||
);
|
||||
const ListItemText = component.find("WithStyles(ListItemText)");
|
||||
const ListItemIcon = component.find("WithStyles(ListItemIcon)");
|
||||
expect(ListItemText).toHaveLength(1);
|
||||
expect(ListItemIcon).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders a DrawerListBase without crashing", () => {
|
||||
shallow(<DrawerListBase t={jest.fn()} classes={{}} theme={{}} />);
|
||||
});
|
||||
|
||||
const userSection = [
|
||||
{ link: "/user/list", label: "navigation.list", icon: <ListIcon /> },
|
||||
{ link: "/user/create", label: "navigation.create", icon: <AddIcon /> },
|
||||
{ link: "/user/edit", label: "navigation.edit", icon: <CreateIcon /> }
|
||||
];
|
||||
|
||||
it("renders correct Links in the DrawerList", () => {
|
||||
const component = shallow(
|
||||
<DrawerListBase
|
||||
t={jest.fn()}
|
||||
classes={{}}
|
||||
theme={{}}
|
||||
header={{ label: "navigation.header.user" }}
|
||||
items={userSection}
|
||||
/>
|
||||
);
|
||||
const Links = component.find("Link");
|
||||
expect(Links).toHaveLength(3);
|
||||
const paths = Links.map(link => link.prop("to"));
|
||||
expect(paths).toContain("/user/list");
|
||||
expect(paths).toContain("/user/create");
|
||||
expect(paths).toContain("/user/edit");
|
||||
});
|
||||
184
src/components/Toolbar.js
Normal file
184
src/components/Toolbar.js
Normal file
@@ -0,0 +1,184 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
Toolbar as MuiToolbar,
|
||||
withStyles,
|
||||
IconButton,
|
||||
Typography
|
||||
} from "material-ui";
|
||||
import AccountCircle from "material-ui-icons/AccountCircle";
|
||||
import MenuIcon from "material-ui-icons/Menu";
|
||||
import { Link } from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
import { openDrawer, logout } from "../redux/actions";
|
||||
import { isDrawerOpen } from "../redux/selectors";
|
||||
import { translate } from "react-i18next";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
const styles = theme => {
|
||||
return {
|
||||
menuButton: {
|
||||
marginLeft: 12,
|
||||
marginRight: 36
|
||||
},
|
||||
hide: {
|
||||
display: "none"
|
||||
},
|
||||
left: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center"
|
||||
},
|
||||
guttersRight: {
|
||||
paddingRight: 24
|
||||
},
|
||||
guttersLeft: {
|
||||
paddingLeft: 24
|
||||
},
|
||||
right: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center"
|
||||
},
|
||||
toolbarContent: {
|
||||
paddingRight: 24
|
||||
},
|
||||
main: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
},
|
||||
title: {
|
||||
textDecoration: "none",
|
||||
color: theme.palette.primary.contrastText
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export class Toolbar 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 });
|
||||
};
|
||||
|
||||
handleProfileClick = () => {
|
||||
const { push } = this.props;
|
||||
push("/profile");
|
||||
this.handleClose();
|
||||
};
|
||||
|
||||
handleLogout = () => {
|
||||
const { logout } = this.props;
|
||||
logout();
|
||||
this.handleClose();
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { classes, className, openDrawer, t, open } = this.props;
|
||||
const { anchorEl } = this.state;
|
||||
const menuOpen = Boolean(anchorEl);
|
||||
return (
|
||||
<MuiToolbar
|
||||
disableGutters={true}
|
||||
className={classNames(
|
||||
classes.guttersRight,
|
||||
open && classes.guttersLeft
|
||||
)}
|
||||
>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.left}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={openDrawer}
|
||||
className={classNames(
|
||||
classes.menuButton,
|
||||
open && classes.hide,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Link className={classes.title} to="/">
|
||||
<Typography variant="title" color="inherit" noWrap>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.right}>
|
||||
<IconButton
|
||||
aria-owns={menuOpen ? "menu-appbar" : null}
|
||||
aria-haspopup="true"
|
||||
onClick={this.handleMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right"
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right"
|
||||
}}
|
||||
open={menuOpen}
|
||||
onClose={this.handleClose}
|
||||
>
|
||||
<MenuItem onClick={this.handleProfileClick}>Profile</MenuItem>
|
||||
<MenuItem onClick={this.handleLogout}>Logout</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</MuiToolbar>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Toolbar.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
openDrawer: PropTypes.func.isRequired,
|
||||
logout: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
open: isDrawerOpen(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openDrawer,
|
||||
logout,
|
||||
push
|
||||
};
|
||||
|
||||
const ConnnectedToolbar = connect(mapStateToProps, mapDispatchToProps)(
|
||||
withStyles(styles, { withTheme: true })(translate()(Toolbar))
|
||||
);
|
||||
|
||||
export default ConnnectedToolbar;
|
||||
34
src/components/Toolbar.test.js
Normal file
34
src/components/Toolbar.test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { Toolbar } from "./Toolbar";
|
||||
|
||||
const props = {
|
||||
t: jest.fn(),
|
||||
classes: {},
|
||||
theme: {},
|
||||
logout: jest.fn(),
|
||||
openDrawer: jest.fn(),
|
||||
open: false
|
||||
};
|
||||
|
||||
it("renders without crashing", () => {
|
||||
shallow(<Toolbar {...props} />);
|
||||
});
|
||||
|
||||
it("renders an multiple MenuItems", () => {
|
||||
const component = shallow(<Toolbar {...props} />);
|
||||
const MenuItems = component.find("WithStyles(MenuItem)");
|
||||
expect(MenuItems.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("renders a Menu", () => {
|
||||
const wrapper = shallow(<Toolbar {...props} />);
|
||||
const component = wrapper.find("WithStyles(Menu)");
|
||||
expect(component).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders 2 IconButtons", () => {
|
||||
const wrapper = shallow(<Toolbar {...props} />);
|
||||
const component = wrapper.find("WithStyles(IconButton)");
|
||||
expect(component).toHaveLength(2);
|
||||
});
|
||||
200
src/components/UserCreateForm.js
Normal file
200
src/components/UserCreateForm.js
Normal file
@@ -0,0 +1,200 @@
|
||||
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 Checkbox from "./Checkbox";
|
||||
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 }) => (
|
||||
<TextField {...rest} {...input} error={touched && error} />
|
||||
);
|
||||
|
||||
export class UserCreateForm extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
handleDelete,
|
||||
handleSubmit,
|
||||
roles,
|
||||
groups,
|
||||
contacts,
|
||||
multiRoles,
|
||||
multiGroups,
|
||||
singleContact,
|
||||
handleChange,
|
||||
handleChangeSingle,
|
||||
handleChangeCheckbox,
|
||||
checkedAdmin,
|
||||
checkedEnabled,
|
||||
canSubmit,
|
||||
id,
|
||||
t
|
||||
} = this.props;
|
||||
var validID = !!id || id === 0;
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classes.userCreateForm}>
|
||||
<Paper className={classes.root} elevation={4}>
|
||||
<Typography type="headline" component="h2">
|
||||
{!!validID
|
||||
? t("user.edit_user_header")
|
||||
: t("user.create_user_header")}
|
||||
</Typography>
|
||||
<div className={classes.formRow}>
|
||||
<Field
|
||||
label={t("user.username")}
|
||||
name="username"
|
||||
component={renderTextField}
|
||||
className={classes.formItem}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<Field
|
||||
label={
|
||||
!!validID ? t("user.password_msg_edit") : t("user.password")
|
||||
}
|
||||
name="password"
|
||||
type="password"
|
||||
component={renderTextField}
|
||||
className={classes.formItem}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectRoles"}
|
||||
options={roles}
|
||||
canAddMultipleValues={true}
|
||||
handleChangeMulti={handleChange("multiRoles")}
|
||||
multi={multiRoles}
|
||||
placeholder={t("user.roles")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectGroups"}
|
||||
options={groups}
|
||||
canAddMultipleValues={true}
|
||||
handleChangeMulti={handleChange("multiGroups")}
|
||||
multi={multiGroups}
|
||||
placeholder={t("user.select_groups")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<ReactSelect
|
||||
className={classes.formItem}
|
||||
name={"selectContact"}
|
||||
options={contacts}
|
||||
handleChangeMulti={handleChangeSingle("singleContact")}
|
||||
multi={singleContact}
|
||||
canAddMultipleValues={false}
|
||||
placeholder={t("user.select_one_contact")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.formRow}>
|
||||
<div className={classes.formItem}>
|
||||
<Checkbox
|
||||
checked={checkedAdmin}
|
||||
name={"checkedAdmin"}
|
||||
onChange={handleChangeCheckbox}
|
||||
label={t("user.checkBoxAdmin")}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<Checkbox
|
||||
checked={checkedEnabled}
|
||||
name={"checkedEnabled"}
|
||||
onChange={handleChangeCheckbox}
|
||||
label={t("user.checkBoxEnabled")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classes.buttonsRow}>
|
||||
<div className={classes.buttonRow}>
|
||||
<Link className={classes.cancelButton} to="/user/list">
|
||||
<Button color="primary">{t("user.cancel")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{!!validID ? (
|
||||
<div className={classes.buttonRow}>
|
||||
<Button onClick={handleDelete} color="primary">
|
||||
{t("user.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={classes.buttonRow}>
|
||||
<Button color="primary" type="submit" disabled={!canSubmit}>
|
||||
{!!validID ? t("user.save_user") : t("user.add_user")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserCreateForm.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default reduxForm({ form: "UserCreateForm", enableReinitialize: true })(
|
||||
withStyles(styles)(translate()(UserCreateForm))
|
||||
);
|
||||
353
src/components/UserCreateScreen.js
Normal file
353
src/components/UserCreateScreen.js
Normal file
@@ -0,0 +1,353 @@
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { CircularProgress, withStyles, Snackbar } from "material-ui";
|
||||
|
||||
import UserCreateForm from "./UserCreateForm";
|
||||
import {
|
||||
fetchList,
|
||||
fetchCreateByObject,
|
||||
fetchDeleteById,
|
||||
fetchUpdateByObject,
|
||||
fetchDetailById,
|
||||
resetUserCreateState
|
||||
} from "../redux/actions";
|
||||
import {
|
||||
getUsers,
|
||||
getTimeFetchedUsers,
|
||||
isFetchingUsers,
|
||||
getGroups,
|
||||
getRoles,
|
||||
getUserFormValues,
|
||||
isCreatingUser,
|
||||
isEditedUser,
|
||||
getTimeFetchedUserList,
|
||||
getCreateUserError,
|
||||
getUserById,
|
||||
isLoadedUsers,
|
||||
isLoadedUser,
|
||||
isUserWithErrors
|
||||
} 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 UserCreateScreen extends Component {
|
||||
state = {
|
||||
singleContact: "",
|
||||
multiRoles: [],
|
||||
multiGroups: [],
|
||||
username: "",
|
||||
password: "",
|
||||
checkedAdmin: false,
|
||||
checkedEnabled: false,
|
||||
setFormValues: false
|
||||
};
|
||||
|
||||
handleChange = name => value => {
|
||||
this.setState({
|
||||
[name]: !!value ? value.split(",").map(v => parseInt(v, 10)) : []
|
||||
});
|
||||
};
|
||||
|
||||
handleChangeSingle = name => value => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
|
||||
handleChangeCheckbox = (name, checked) => {
|
||||
this.setState({ [name]: checked });
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
const {
|
||||
fetchUsers,
|
||||
fetchContacts,
|
||||
fetchRoles,
|
||||
fetchGroups,
|
||||
fetchUser,
|
||||
id
|
||||
} = this.props;
|
||||
fetchUsers({});
|
||||
fetchContacts({});
|
||||
fetchRoles({});
|
||||
fetchGroups({});
|
||||
if (!!id || id === 0) {
|
||||
fetchUser(id);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.setState({
|
||||
singleContact: "",
|
||||
multiRoles: [],
|
||||
multiGroups: [],
|
||||
username: "",
|
||||
password: "",
|
||||
checkedAdmin: false,
|
||||
checkedEnabled: false,
|
||||
setFormValues: false
|
||||
});
|
||||
const { resetUserCreateState } = this.props;
|
||||
resetUserCreateState();
|
||||
};
|
||||
|
||||
componentWillReceiveProps = props => {
|
||||
const { id, userForEdit, isLoadedUser, isLoadedUsers } = props;
|
||||
const { setFormValues } = this.state;
|
||||
|
||||
if (
|
||||
(parseInt(id, 10) || parseInt(id, 10) === 0) &&
|
||||
!!isLoadedUser &&
|
||||
!!isLoadedUsers &&
|
||||
!setFormValues
|
||||
) {
|
||||
this.setState({
|
||||
username: userForEdit.username || "",
|
||||
singleContact: parseInt(userForEdit.contact, 10) || "",
|
||||
multiRoles: userForEdit.roles || [],
|
||||
multiGroups: userForEdit.groups || [],
|
||||
checkedAdmin: userForEdit.admin || false,
|
||||
checkedEnabled: userForEdit.enabled || false,
|
||||
setFormValues: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleDelete = () => {
|
||||
const { fetchDeleteUser, id } = this.props;
|
||||
fetchDeleteUser(id);
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
const {
|
||||
fetchCreateUser,
|
||||
fetchEditUser,
|
||||
values: { username, password },
|
||||
id
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
singleContact,
|
||||
multiRoles,
|
||||
multiGroups,
|
||||
checkedAdmin,
|
||||
checkedEnabled
|
||||
} = this.state;
|
||||
|
||||
var data = {
|
||||
username,
|
||||
password,
|
||||
enabled: checkedAdmin,
|
||||
admin: checkedEnabled,
|
||||
roles: multiRoles,
|
||||
groups: multiGroups
|
||||
};
|
||||
|
||||
//TODO replace enabled and admin with variables
|
||||
if (!!id || id === 0) {
|
||||
data.id = id;
|
||||
if (!!singleContact || singleContact === 0) {
|
||||
data.contact = parseInt(singleContact, 10);
|
||||
}
|
||||
fetchEditUser(data);
|
||||
} else {
|
||||
if (!!singleContact || singleContact === 0) {
|
||||
data.contact = parseInt(singleContact, 10);
|
||||
}
|
||||
fetchCreateUser(data);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
contacts,
|
||||
roles,
|
||||
groups,
|
||||
isLoaded,
|
||||
classes,
|
||||
isSent,
|
||||
isEdited,
|
||||
isUserWithErrors,
|
||||
values,
|
||||
id,
|
||||
t
|
||||
} = this.props;
|
||||
const {
|
||||
username,
|
||||
singleContact,
|
||||
multiRoles,
|
||||
multiGroups,
|
||||
checkedAdmin,
|
||||
checkedEnabled
|
||||
} = this.state;
|
||||
|
||||
console.log();
|
||||
var alert = null;
|
||||
if (!!id || id === 0) {
|
||||
alert = isEdited ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{t("user.user_is_edited_msg")}
|
||||
</div>
|
||||
) : null;
|
||||
} else {
|
||||
alert = isEdited ? (
|
||||
<div style={{ textAlign: "center" }}>{t("user.user_created_msg")}</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const mappedRoles = (roles || []).map(role => {
|
||||
return {
|
||||
value: role.id,
|
||||
label: role.name
|
||||
};
|
||||
});
|
||||
|
||||
const mappedGroups = (groups || []).map(group => {
|
||||
return {
|
||||
value: group.id,
|
||||
label: group.name
|
||||
};
|
||||
});
|
||||
|
||||
const mappedContacts = (contacts || []).map(contact => {
|
||||
return {
|
||||
value: contact.id,
|
||||
label: contact.lastName + ", " + contact.firstName
|
||||
};
|
||||
});
|
||||
|
||||
const loadingScreen = (
|
||||
<Screen>
|
||||
<div className={classes.progressContainer}>
|
||||
<CircularProgress className={classes.progress} />
|
||||
</div>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
if (id) {
|
||||
if (!isLoaded) {
|
||||
return loadingScreen;
|
||||
}
|
||||
}
|
||||
|
||||
if (!!id && !isSent && !isLoaded) {
|
||||
return loadingScreen;
|
||||
}
|
||||
// we don't show password in edit form, so it's not necessary to update it
|
||||
var canSubmit;
|
||||
if (!!id || id === 0) {
|
||||
canSubmit = values && values.username ? true : false;
|
||||
} else {
|
||||
canSubmit = values && values.username && values.password ? true : false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<UserCreateForm
|
||||
initialValues={{ username: username }}
|
||||
multiRoles={multiRoles}
|
||||
multiGroups={multiGroups}
|
||||
singleContact={singleContact}
|
||||
handleChange={this.handleChange}
|
||||
handleChangeSingle={this.handleChangeSingle}
|
||||
handleChangeCheckbox={this.handleChangeCheckbox}
|
||||
checkedAdmin={checkedAdmin}
|
||||
checkedEnabled={checkedEnabled}
|
||||
roles={mappedRoles}
|
||||
groups={mappedGroups}
|
||||
contacts={mappedContacts}
|
||||
handleDelete={this.handleDelete}
|
||||
ref={form => (this.form = form)}
|
||||
onSubmit={this.handleSubmit}
|
||||
canSubmit={canSubmit}
|
||||
id={id}
|
||||
/>
|
||||
{alert}
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
open={isUserWithErrors}
|
||||
onClose={this.handleClose}
|
||||
SnackbarContentProps={{
|
||||
"aria-describedby": "message-id"
|
||||
}}
|
||||
message={<span id="message-id">{t("user.error_message")}</span>}
|
||||
/>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const id = parseInt(ownProps.match.params.id, 10);
|
||||
const isFetching = isFetchingUsers(state);
|
||||
//todo replace with timeFetchedUsers
|
||||
const timeFetchedUsers = getTimeFetchedUsers(state);
|
||||
|
||||
const isCreating = isCreatingUser(state);
|
||||
const isEdited = isEditedUser(state);
|
||||
const groupCreatedTime = getTimeFetchedUserList(state);
|
||||
const createUserError = getCreateUserError(state);
|
||||
return {
|
||||
users: getUsers(state),
|
||||
roles: getRoles(state),
|
||||
groups: getGroups(state),
|
||||
contacts: getContacts(state),
|
||||
|
||||
values: getUserFormValues(state),
|
||||
isFetching,
|
||||
isLoaded: !isFetching && !!timeFetchedUsers,
|
||||
isSent: !isCreating && !!groupCreatedTime && !createUserError,
|
||||
isEdited,
|
||||
id,
|
||||
userForEdit: getUserById(id)(state),
|
||||
isLoadedUsers: isLoadedUsers(state),
|
||||
isLoadedUser: isLoadedUser(state),
|
||||
isUserWithErrors: isUserWithErrors(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchUsers: fetchList(entity.user),
|
||||
fetchRoles: fetchList(entity.role),
|
||||
fetchGroups: fetchList(entity.group),
|
||||
fetchContacts: fetchList(entity.contact),
|
||||
|
||||
fetchCreateUser: fetchCreateByObject(entity.user),
|
||||
fetchUser: fetchDetailById(entity.user),
|
||||
fetchEditUser: fetchUpdateByObject(entity.user),
|
||||
resetUserCreateState,
|
||||
fetchDeleteUser: fetchDeleteById(entity.user)
|
||||
};
|
||||
|
||||
UserCreateScreen.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export const ConnectedUserCreateScreen = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UserCreateScreen);
|
||||
|
||||
export default withStyles(styles)(translate()(ConnectedUserCreateScreen));
|
||||
199
src/components/UserList.js
Normal file
199
src/components/UserList.js
Normal file
@@ -0,0 +1,199 @@
|
||||
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));
|
||||
22
src/config/groupPermissionTypes.js
Normal file
22
src/config/groupPermissionTypes.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export const permission = [
|
||||
{
|
||||
value: "READ",
|
||||
label: "Read"
|
||||
},
|
||||
{
|
||||
value: "WRITE",
|
||||
label: "Write"
|
||||
},
|
||||
{
|
||||
value: "UPDATE",
|
||||
label: "Update"
|
||||
},
|
||||
{
|
||||
value: "DELETE",
|
||||
label: "Delete"
|
||||
},
|
||||
{
|
||||
value: "ADMINISTRATION",
|
||||
label: "Administration"
|
||||
}
|
||||
];
|
||||
34
src/config/index.js
Normal file
34
src/config/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export const apiMethod = {
|
||||
list: "list",
|
||||
detail: "detail",
|
||||
create: "create",
|
||||
update: "update",
|
||||
delete: "delete"
|
||||
};
|
||||
|
||||
export const apiHttpMethodMapping = {
|
||||
list: "get",
|
||||
detail: "get",
|
||||
create: "post",
|
||||
update: "put",
|
||||
delete: "del"
|
||||
};
|
||||
|
||||
export const apiActionType = {
|
||||
request: "REQUEST",
|
||||
success: "SUCCESS",
|
||||
failure: "FAILURE"
|
||||
};
|
||||
|
||||
export const config = {
|
||||
apiBaseUrl:
|
||||
process.env.NODE_ENV === "development"
|
||||
? "https://api.swt.leoek.eu"
|
||||
: "https://api.swt.leoek.eu",
|
||||
ERROR: {
|
||||
NOCONNECTION: "NOCONNECTION",
|
||||
UNAUTHORIZED: "UNAUTHORIZED"
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
src/i18n/de.js
Normal file
8
src/i18n/de.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
translation: {
|
||||
hello: "Hallo",
|
||||
login: {
|
||||
button_login: "Einloggen"
|
||||
}
|
||||
}
|
||||
};
|
||||
100
src/i18n/en.js
Normal file
100
src/i18n/en.js
Normal file
@@ -0,0 +1,100 @@
|
||||
export default {
|
||||
translation: {
|
||||
hello: "Hello",
|
||||
title: "Mitgliederverwaltung",
|
||||
cancel: "Cancel",
|
||||
login: {
|
||||
button_login: "Login",
|
||||
label_username: "Email",
|
||||
label_password: "Password"
|
||||
},
|
||||
navigation: {
|
||||
header: {
|
||||
user: "User",
|
||||
group: "Group",
|
||||
contact: "Contact"
|
||||
},
|
||||
create: "Create",
|
||||
edit: "Edit",
|
||||
list: "List"
|
||||
},
|
||||
user_create_screen: {
|
||||
create_user: "Create User"
|
||||
},
|
||||
user: {
|
||||
cancel: "Cancel",
|
||||
delete: "Delete",
|
||||
error_message: "Oops! Something went wrong",
|
||||
edit_user_header: "Edit user",
|
||||
create_user_header: "Create new user",
|
||||
save_user: "Save user",
|
||||
add_user: "Add user",
|
||||
user_is_edited_msg: "User is edited!",
|
||||
user_created_msg: "New User created!",
|
||||
username: "Username (Email)",
|
||||
password: "Password",
|
||||
password_msg_edit: "A new password if needed",
|
||||
roles: "Select roles",
|
||||
select_groups: "Select groups",
|
||||
select_one_contact: "Select one contact",
|
||||
checkBoxAdmin: "Admin",
|
||||
checkBoxEnabled: "Enabled"
|
||||
},
|
||||
role: {
|
||||
cancel: "Cancel",
|
||||
delete: "Delete",
|
||||
error_message: "Oops! Something went wrong",
|
||||
edit_role_header: "Edit role",
|
||||
create_role_header: "Create new role",
|
||||
save_role: "Save role",
|
||||
add_role: "Add role",
|
||||
role_is_edited_msg: "Role is edited!",
|
||||
role_created_msg: "New Role created!",
|
||||
role_name: "Role name",
|
||||
role_description: "Role description",
|
||||
select_permissions: "Select permissions",
|
||||
select_users: "Select users"
|
||||
},
|
||||
role_list: {
|
||||
header: "Role list",
|
||||
name: "Name",
|
||||
description: "Description",
|
||||
permissions: "Permissions",
|
||||
users: "Users",
|
||||
edit: "Edit",
|
||||
delete: "Delete"
|
||||
},
|
||||
group_list: {
|
||||
header: "Group list",
|
||||
name: "Name",
|
||||
permission: "Permission",
|
||||
contacts: "Contacts",
|
||||
responsibles: "Responsibles",
|
||||
users: "Users",
|
||||
edit: "Edit",
|
||||
delete: "Delete"
|
||||
},
|
||||
group: {
|
||||
cancel: "Cancel",
|
||||
delete: "Delete",
|
||||
error_message: "Oops! Something went wrong",
|
||||
edit_group_header: "Edit group",
|
||||
create_group_header: "Create new group",
|
||||
save_group: "Save group",
|
||||
add_group: "Add group",
|
||||
group_is_edited_msg: "Group is edited!",
|
||||
group_created_msg: "New Group created!",
|
||||
group_name: "Group name",
|
||||
group_description: "Group description",
|
||||
select_one_permission: "Select one permission",
|
||||
select_contacts: "Select contacts",
|
||||
select_responsibles: "Select responsibles",
|
||||
select_users: "Select users"
|
||||
},
|
||||
user_list: {
|
||||
header: "User List",
|
||||
name: "Name",
|
||||
edit: "Edit"
|
||||
}
|
||||
}
|
||||
};
|
||||
30
src/i18n/i18n.js
Normal file
30
src/i18n/i18n.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import i18n from "i18next";
|
||||
import XHR from "i18next-xhr-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import translations from "./translations";
|
||||
|
||||
i18n
|
||||
.use(XHR)
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react!!
|
||||
},
|
||||
|
||||
// react i18next special options (optional)
|
||||
react: {
|
||||
wait: false, // set to true if you like to wait for loaded in every translated hoc
|
||||
nsMode: "default" // set it to fallback to let passed namespaces to translated hoc act as fallbacks
|
||||
},
|
||||
|
||||
resources: translations
|
||||
});
|
||||
|
||||
export const changeLanguage = lng => {
|
||||
i18n.changeLanguage(lng);
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
6
src/i18n/index.js
Normal file
6
src/i18n/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import i18n, { changeLanguage } from "./i18n"; // initialized i18next instance
|
||||
import translations from "./translations";
|
||||
|
||||
export { i18n, translations, changeLanguage };
|
||||
|
||||
export default i18n;
|
||||
7
src/i18n/translations.js
Normal file
7
src/i18n/translations.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import en from "./en.js";
|
||||
import de from "./de.js";
|
||||
|
||||
export default {
|
||||
en,
|
||||
de
|
||||
};
|
||||
5
src/index.css
Normal file
5
src/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
11
src/index.js
Normal file
11
src/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import registerServiceWorker from "./registerServiceWorker";
|
||||
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
import "typeface-roboto";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
||||
registerServiceWorker();
|
||||
114
src/lib/api.js
Normal file
114
src/lib/api.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import config from "../config";
|
||||
|
||||
const getErrorPromise = error => {
|
||||
console.log("Handled Connection Error", error);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
ok: false,
|
||||
json: () =>
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(error);
|
||||
})
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleFetchErrors = (errorMessage, statusCode) => {
|
||||
const error = {
|
||||
statusCode: 503,
|
||||
devMessage: "An error ocurred while trying to connect to the API.",
|
||||
error: config.ERROR.NOCONNECTION
|
||||
};
|
||||
return getErrorPromise(error);
|
||||
};
|
||||
|
||||
const getQueryFromParams = (parameters = {}) =>
|
||||
Object.keys(parameters || {}).reduce((result, parameter) => {
|
||||
if (!parameter || !parameters[parameter]) return result;
|
||||
return result === ""
|
||||
? `${parameter}=${parameters[parameter]}`
|
||||
: `${result}&${parameter}=${parameters[parameter]}`;
|
||||
}, "");
|
||||
|
||||
const getHeaders = token => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
};
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return new Headers(headers);
|
||||
};
|
||||
|
||||
const get = async (request, token) => {
|
||||
const { endpoint, parameters } = request;
|
||||
const query = getQueryFromParams(parameters);
|
||||
return fetch(`${config.apiBaseUrl}/${endpoint}?${query}`, {
|
||||
method: "get",
|
||||
headers: getHeaders(token)
|
||||
}).catch(handleFetchErrors);
|
||||
};
|
||||
|
||||
const post = async (request, token) => {
|
||||
const { endpoint, parameters, data } = request;
|
||||
const query = getQueryFromParams(parameters);
|
||||
return fetch(`${config.apiBaseUrl}/${endpoint}?${query}`, {
|
||||
method: "post",
|
||||
body: JSON.stringify(data),
|
||||
headers: getHeaders(token)
|
||||
}).catch(handleFetchErrors);
|
||||
};
|
||||
|
||||
const put = async (request, token) => {
|
||||
const { endpoint, parameters, data } = request;
|
||||
const query = getQueryFromParams(parameters);
|
||||
return fetch(`${config.apiBaseUrl}/${endpoint}?${query}`, {
|
||||
method: "put",
|
||||
body: JSON.stringify(data),
|
||||
headers: getHeaders(token)
|
||||
}).catch(handleFetchErrors);
|
||||
};
|
||||
|
||||
const del = async (request, token) => {
|
||||
const { endpoint, parameters, data } = request;
|
||||
const query = getQueryFromParams(parameters);
|
||||
return fetch(`${config.apiBaseUrl}/${endpoint}?${query}`, {
|
||||
method: "delete",
|
||||
body: JSON.stringify(data),
|
||||
headers: getHeaders(token)
|
||||
}).catch(handleFetchErrors);
|
||||
};
|
||||
|
||||
/**
|
||||
* use this function to create a new api template which stores username and password
|
||||
*/
|
||||
const createApi = token => ({
|
||||
setToken: token => {
|
||||
this.token = token;
|
||||
},
|
||||
getToken: () => this.token,
|
||||
get: request => get(request, this.token),
|
||||
post: request => post(request, this.token),
|
||||
put: request => put(request, this.token),
|
||||
del: request => del(request, this.token)
|
||||
});
|
||||
|
||||
/**
|
||||
* use this function to create a new api objet.
|
||||
* @param {*} baseUrl
|
||||
* @param {*} username
|
||||
* @param {*} password
|
||||
*/
|
||||
const create = (baseUrl = config.apiBaseUrl, token) => {
|
||||
const api = createApi();
|
||||
|
||||
if (token) {
|
||||
api.setToken(token);
|
||||
}
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
export default {
|
||||
create
|
||||
};
|
||||
97
src/lib/entity.js
Normal file
97
src/lib/entity.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { isEmpty, unionBy } from "lodash";
|
||||
import { apiMethod } from "../config";
|
||||
|
||||
export const checkEntity = (entity1 = {}) => (entity2 = {}) =>
|
||||
entity1.name && !isEmpty(entity1.name) && entity1.name === entity2.name;
|
||||
|
||||
export const getInitialEntityState = entity => {
|
||||
const state = {
|
||||
entity,
|
||||
items: []
|
||||
};
|
||||
Object.values(apiMethod).forEach(method => {
|
||||
state[method] = {
|
||||
isFetching: false,
|
||||
timeFetched: null,
|
||||
items: null,
|
||||
meta: null,
|
||||
edited: null
|
||||
};
|
||||
});
|
||||
return state;
|
||||
};
|
||||
|
||||
export const mergefetchRequest = action => state => {
|
||||
const { method } = action;
|
||||
if (apiMethod[method]) {
|
||||
return {
|
||||
...state,
|
||||
[method]: {
|
||||
...state[method],
|
||||
isFetching: true,
|
||||
error: null
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
export const mergefetchSuccess = action => state => {
|
||||
const { method, payload = {} } = action;
|
||||
const { data: { content, ...rest }, timeFetched } = payload;
|
||||
const items = Array.isArray(content) ? content : [content];
|
||||
if (apiMethod[method]) {
|
||||
return {
|
||||
...state,
|
||||
items: unionBy(items, state.items, "id"),
|
||||
[method]: {
|
||||
...state[method],
|
||||
isFetching: false,
|
||||
items: items.map(item => item && item.id),
|
||||
meta: Array.isArray(content) ? rest : null,
|
||||
timeFetched
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
export const mergefetchFailure = action => state => {
|
||||
const { method, payload = {} } = action;
|
||||
const { error, timeFetched } = payload;
|
||||
if (apiMethod[method]) {
|
||||
return {
|
||||
...state,
|
||||
[method]: {
|
||||
...state[method],
|
||||
isFetching: false,
|
||||
error,
|
||||
timeFetched
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const entity = {
|
||||
user: {
|
||||
name: "user",
|
||||
endpoint: "users"
|
||||
},
|
||||
contact: {
|
||||
name: "contact",
|
||||
endpoint: "contacts"
|
||||
},
|
||||
permission: {
|
||||
name: "permission",
|
||||
endpoint: "permissions"
|
||||
},
|
||||
role: {
|
||||
name: "role",
|
||||
endpoint: "roles"
|
||||
},
|
||||
group: {
|
||||
name: "group",
|
||||
endpoint: "groups"
|
||||
}
|
||||
};
|
||||
|
||||
export default entity;
|
||||
7
src/logo.svg
Normal file
7
src/logo.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
119
src/mocks/apiResponses.js
Normal file
119
src/mocks/apiResponses.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import config from "../config";
|
||||
|
||||
export const apiResponse_ErrorNoConnection = {
|
||||
code: 503,
|
||||
devMessage: "An error ocurred while trying to connect to the API.",
|
||||
error: config.ERROR.NOCONNECTION
|
||||
};
|
||||
|
||||
export const apiResponse_user = {
|
||||
last: false,
|
||||
totalPages: 11,
|
||||
totalElements: 51,
|
||||
sort: null,
|
||||
numberOfElements: 5,
|
||||
first: true,
|
||||
size: 5,
|
||||
number: 0,
|
||||
content: [
|
||||
{
|
||||
id: 0,
|
||||
username: "user0",
|
||||
firstName: "Max",
|
||||
lastName: "Mustermann",
|
||||
email: "max.mustermann@mail.de",
|
||||
password: "string",
|
||||
phone: "string",
|
||||
userStatus: 0,
|
||||
groups: [
|
||||
{
|
||||
id: 0,
|
||||
name: "string",
|
||||
permissions: [
|
||||
{
|
||||
id: 0,
|
||||
name: "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
username: "user1",
|
||||
firstName: "Max",
|
||||
lastName: "Mustermann",
|
||||
email: "max.mustermann@mail.de",
|
||||
password: "string",
|
||||
phone: "string",
|
||||
userStatus: 0,
|
||||
groups: [
|
||||
{
|
||||
id: 0,
|
||||
name: "string",
|
||||
permissions: [
|
||||
{
|
||||
id: 0,
|
||||
name: "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const apiResponse_role = {
|
||||
content: [
|
||||
{
|
||||
id: 0,
|
||||
name: "Test Role 1",
|
||||
description: "Description 1",
|
||||
permissions: [
|
||||
{
|
||||
id: 0,
|
||||
name: "Permission 1"
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Permission 2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Test Role 2",
|
||||
description: "Description 2",
|
||||
permissions: [
|
||||
{
|
||||
id: 4,
|
||||
name: "Permission 5"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Permission 6"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const apiResponse_createRole = {
|
||||
content: [
|
||||
{
|
||||
id: 0,
|
||||
name: "Test Role 1",
|
||||
description: "Description 1",
|
||||
permissions: [
|
||||
{
|
||||
id: 0,
|
||||
name: "Permission 1"
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Permission 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
32
src/mocks/index.js
Normal file
32
src/mocks/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export * from "./apiResponses";
|
||||
|
||||
export const initialState = {
|
||||
login: {
|
||||
isLoggedIn: false
|
||||
},
|
||||
ui: {
|
||||
drawer: {
|
||||
open: true
|
||||
}
|
||||
},
|
||||
user: {
|
||||
isFetching: false,
|
||||
timeFetched: null,
|
||||
items: null,
|
||||
meta: null,
|
||||
error: null
|
||||
},
|
||||
form: {},
|
||||
role: {
|
||||
isFetching: false,
|
||||
timeFetched: null,
|
||||
items: null,
|
||||
meta: null,
|
||||
create: {
|
||||
isFetching: false,
|
||||
timeFetched: null,
|
||||
items: null,
|
||||
edited: null
|
||||
}
|
||||
}
|
||||
};
|
||||
85
src/redux/actions/apiActions.js
Normal file
85
src/redux/actions/apiActions.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { apiActionType, apiMethod } from "../../config";
|
||||
|
||||
//API Actions
|
||||
export const CONNECTION_FAILURE = "CONNECTION_FAILURE";
|
||||
export const RESET_NO_CONNECTION = "RESET_NO_CONNECTION";
|
||||
|
||||
export const FETCH_LIST_REQUEST = "API/FETCH_LIST_REQUEST";
|
||||
export const FETCH_LIST_SUCCESS = "API/FETCH_LIST_SUCCESS";
|
||||
export const FETCH_LIST_FAILURE = "API/FETCH_LIST_FAILURE";
|
||||
|
||||
// login actions
|
||||
export const FETCH_LOGIN_REQUEST = "FETCH_LOGIN_REQUEST";
|
||||
export const FETCH_LOGIN_SUCCESS = "FETCH_LOGIN_SUCCESS";
|
||||
export const FETCH_LOGIN_FAILURE = "FETCH_LOGIN_FAILURE";
|
||||
|
||||
export const FETCH_GENERIC = (type = apiActionType.request) =>
|
||||
`API/FETCH_GENERIC_${type}`;
|
||||
export const FETCH_GENERIC_REQUEST = FETCH_GENERIC(apiActionType.request);
|
||||
export const fetchGeneric = method => entity => payload => ({
|
||||
type: FETCH_GENERIC_REQUEST,
|
||||
method,
|
||||
entity,
|
||||
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 FETCH_GENERIC_FAILURE = FETCH_GENERIC(apiActionType.failure);
|
||||
export const fetchGenericFailure = method => entity => error => ({
|
||||
type: FETCH_GENERIC_FAILURE,
|
||||
method,
|
||||
entity,
|
||||
payload: {
|
||||
timeFetched: new Date(),
|
||||
error
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchList = entity => parameters =>
|
||||
fetchGeneric(apiMethod.list)(entity)({ parameters });
|
||||
export const fetchListSuccess = fetchGenericSuccess(apiMethod.list);
|
||||
export const fetchListFailure = fetchGenericFailure(apiMethod.list);
|
||||
export const fetchDetail = fetchGeneric(apiMethod.detail);
|
||||
export const fetchDetailById = entity => id =>
|
||||
fetchGeneric(apiMethod.detail)(entity)({ data: { id } });
|
||||
export const fetchUpdate = fetchGeneric(apiMethod.update);
|
||||
export const fetchUpdateByObject = entity => data =>
|
||||
fetchGeneric(apiMethod.update)(entity)({ data });
|
||||
export const fetchDelete = fetchGeneric(apiMethod.delete);
|
||||
export const fetchDeleteById = entity => id =>
|
||||
fetchGeneric(apiMethod.delete)(entity)({ data: { id } });
|
||||
export const fetchCreate = fetchGeneric(apiMethod.create);
|
||||
export const fetchCreateByObject = entity => data =>
|
||||
fetchGeneric(apiMethod.create)(entity)({ data });
|
||||
|
||||
export const fetchLogin = (username, password) => ({
|
||||
type: FETCH_LOGIN_REQUEST,
|
||||
payload: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchLoginSuccess = data => ({
|
||||
type: FETCH_LOGIN_SUCCESS,
|
||||
payload: {
|
||||
timeFetched: new Date(),
|
||||
data
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchLoginFailure = error => ({
|
||||
type: FETCH_LOGIN_FAILURE,
|
||||
payload: {
|
||||
timeFetched: new Date(),
|
||||
error
|
||||
}
|
||||
});
|
||||
4
src/redux/actions/groupActions.js
Normal file
4
src/redux/actions/groupActions.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const RESET_GROUP_CREATE_STATE = "RESET_GROUP_CREATE_STATE";
|
||||
export const resetGroupCreateState = () => ({
|
||||
type: RESET_GROUP_CREATE_STATE
|
||||
});
|
||||
11
src/redux/actions/index.js
Normal file
11
src/redux/actions/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./apiActions";
|
||||
export * from "./loginActions";
|
||||
export * from "./uiActions";
|
||||
export * from "./groupActions.js";
|
||||
export * from "./roleActions.js";
|
||||
export * from "./userActions.js";
|
||||
|
||||
export const REDUX_REHYDRATION_COMPLETED = "REDUX_REHYDRATION_COMPLETED";
|
||||
export const reduxRehydrationCompleted = () => ({
|
||||
type: REDUX_REHYDRATION_COMPLETED
|
||||
});
|
||||
6
src/redux/actions/loginActions.js
Normal file
6
src/redux/actions/loginActions.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const LOGOUT = "LOGOUT";
|
||||
export const LOGIN = "LOGIN";
|
||||
|
||||
export const logout = () => ({
|
||||
type: LOGOUT
|
||||
});
|
||||
4
src/redux/actions/roleActions.js
Normal file
4
src/redux/actions/roleActions.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const RESET_ROLE_CREATE_STATE = "RESET_ROLE_CREATE_STATE";
|
||||
export const resetRoleCreateState = () => ({
|
||||
type: RESET_ROLE_CREATE_STATE
|
||||
});
|
||||
9
src/redux/actions/uiActions.js
Normal file
9
src/redux/actions/uiActions.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const OPEN_DRAWER = "UI/OPEN_DRAWER";
|
||||
export const openDrawer = () => ({
|
||||
type: OPEN_DRAWER
|
||||
});
|
||||
|
||||
export const CLOSE_DRAWER = "UI/CLOSE_DRAWER";
|
||||
export const closeDrawer = () => ({
|
||||
type: CLOSE_DRAWER
|
||||
});
|
||||
4
src/redux/actions/userActions.js
Normal file
4
src/redux/actions/userActions.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const RESET_USER_CREATE_STATE = "RESET_USER_CREATE_STATE";
|
||||
export const resetUserCreateState = () => ({
|
||||
type: RESET_USER_CREATE_STATE
|
||||
});
|
||||
60
src/redux/createStore.js
Normal file
60
src/redux/createStore.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
import { persistStore, persistCombineReducers } from "redux-persist";
|
||||
import storage from "redux-persist/lib/storage";
|
||||
import { createBlacklistFilter } from "redux-persist-transform-filter";
|
||||
import createSagaMiddleware from "redux-saga";
|
||||
|
||||
import createHistory from "history/createBrowserHistory";
|
||||
import { routerReducer, routerMiddleware } from "react-router-redux";
|
||||
|
||||
import reducers from "./reducers";
|
||||
import rootSaga from "./sagas";
|
||||
|
||||
//https://github.com/zalmoxisus/redux-devtools-extension
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
/**
|
||||
* documentation on redux persist filters
|
||||
* https://github.com/edy/redux-persist-transform-filter
|
||||
*/
|
||||
const loginFilter = createBlacklistFilter("login", ["error"]);
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
whitelist: ["login"],
|
||||
transforms: [loginFilter]
|
||||
};
|
||||
|
||||
// creates the store
|
||||
export default () => {
|
||||
// Create a history of your choosing (we're using a browser history in this case)
|
||||
const history = createHistory();
|
||||
|
||||
//create the rootReducer
|
||||
const rootReducer = persistCombineReducers(persistConfig, {
|
||||
...reducers,
|
||||
router: routerReducer
|
||||
});
|
||||
|
||||
// create the saga middleware
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
|
||||
// create the touter middleware, aggregate the middlewares
|
||||
const middlewares = [sagaMiddleware, routerMiddleware(history)];
|
||||
const enhancers = [];
|
||||
enhancers.push(applyMiddleware(...middlewares));
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
undefined,
|
||||
composeEnhancers(...enhancers)
|
||||
);
|
||||
|
||||
//run sagas
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
//start persisting the store
|
||||
const persistor = persistStore(store, null, () => store.getState());
|
||||
|
||||
return { persistor, store, history };
|
||||
};
|
||||
41
src/redux/reducers/apiReducer.js
Normal file
41
src/redux/reducers/apiReducer.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
FETCH_GENERIC_FAILURE,
|
||||
FETCH_GENERIC_SUCCESS,
|
||||
FETCH_GENERIC_REQUEST
|
||||
} from "../actions";
|
||||
import {
|
||||
entity,
|
||||
checkEntity,
|
||||
getInitialEntityState,
|
||||
mergefetchRequest,
|
||||
mergefetchSuccess,
|
||||
mergefetchFailure
|
||||
} from "../../lib/entity";
|
||||
|
||||
export const apiReducerTemplate = entity => (state, action) => {
|
||||
if (checkEntity(entity)(action.entity)) {
|
||||
if (action.type === FETCH_GENERIC_REQUEST) {
|
||||
return mergefetchRequest(action)(state);
|
||||
} else if (action.type === FETCH_GENERIC_SUCCESS) {
|
||||
return mergefetchSuccess(action)(state);
|
||||
} else if (action.type === FETCH_GENERIC_FAILURE) {
|
||||
return mergefetchFailure(action)(state);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const apiReducer = entity => (state = getInitialEntityState(entity), action) =>
|
||||
apiReducerTemplate(entity)(state, action) || state;
|
||||
|
||||
export default apiReducer;
|
||||
|
||||
export const addApiReducers = reducers => {
|
||||
Object.values(entity).forEach(e => {
|
||||
if (!reducers[e.name]) {
|
||||
reducers[e.name] = apiReducer(e);
|
||||
}
|
||||
});
|
||||
return reducers;
|
||||
};
|
||||
24
src/redux/reducers/group.js
Normal file
24
src/redux/reducers/group.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RESET_GROUP_CREATE_STATE } from "../actions";
|
||||
import { apiReducerTemplate } from "./apiReducer";
|
||||
import { entity, getInitialEntityState } from "../../lib/entity";
|
||||
import { apiMethod } from "../../config";
|
||||
|
||||
const getInitialState = () => getInitialEntityState(entity.group);
|
||||
|
||||
const group = (state = getInitialState(), action) => {
|
||||
let newState = apiReducerTemplate(entity.group)(state, action);
|
||||
if (newState) {
|
||||
return newState;
|
||||
}
|
||||
if (action.type === RESET_GROUP_CREATE_STATE) {
|
||||
return {
|
||||
...state,
|
||||
[apiMethod.create]: getInitialState()[apiMethod.create],
|
||||
[apiMethod.detail]: getInitialState()[apiMethod.detail],
|
||||
[apiMethod.update]: getInitialState()[apiMethod.update]
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default group;
|
||||
24
src/redux/reducers/index.js
Normal file
24
src/redux/reducers/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { combineReducers } from "redux";
|
||||
import { reducer as formReducer } from "redux-form";
|
||||
|
||||
import login from "./login";
|
||||
import ui from "./ui";
|
||||
import { addApiReducers } from "./apiReducer";
|
||||
import group from "./group";
|
||||
import role from "./role";
|
||||
import user from "./user";
|
||||
|
||||
const reducers = {
|
||||
login,
|
||||
ui,
|
||||
group,
|
||||
role,
|
||||
user,
|
||||
form: formReducer
|
||||
};
|
||||
|
||||
const enhancedReducers = addApiReducers(reducers);
|
||||
|
||||
export const getRootReducer = combineReducers(enhancedReducers);
|
||||
|
||||
export default enhancedReducers;
|
||||
49
src/redux/reducers/login.js
Normal file
49
src/redux/reducers/login.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
LOGOUT,
|
||||
FETCH_LOGIN_REQUEST,
|
||||
FETCH_LOGIN_SUCCESS,
|
||||
FETCH_LOGIN_FAILURE
|
||||
} from "../actions";
|
||||
|
||||
const initialState = {
|
||||
isLoggedIn: false,
|
||||
isFetching: false,
|
||||
timeFetched: null,
|
||||
access_token: null,
|
||||
token_type: null,
|
||||
expires_in: null,
|
||||
decodedJwt: null
|
||||
};
|
||||
|
||||
const login = (state = initialState, action) => {
|
||||
if (action.type === FETCH_LOGIN_REQUEST) {
|
||||
return {
|
||||
...state,
|
||||
isFetching: true
|
||||
};
|
||||
} else if (action.type === FETCH_LOGIN_SUCCESS) {
|
||||
const { data, timeFetched } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
...data,
|
||||
timeFetched,
|
||||
isLoggedIn: true,
|
||||
isFetching: false
|
||||
};
|
||||
} else if (action.type === FETCH_LOGIN_FAILURE) {
|
||||
const { error, timeFetched } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
error,
|
||||
timeFetched,
|
||||
isLoggedIn: false,
|
||||
isFetching: false
|
||||
};
|
||||
} else if (action.type === LOGOUT) {
|
||||
return initialState;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default login;
|
||||
24
src/redux/reducers/role.js
Normal file
24
src/redux/reducers/role.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RESET_ROLE_CREATE_STATE } from "../actions";
|
||||
import { apiReducerTemplate } from "./apiReducer";
|
||||
import { entity, getInitialEntityState } from "../../lib/entity";
|
||||
import { apiMethod } from "../../config";
|
||||
|
||||
const getInitialState = () => getInitialEntityState(entity.role);
|
||||
|
||||
const role = (state = getInitialState(), action) => {
|
||||
let newState = apiReducerTemplate(entity.role)(state, action);
|
||||
if (newState) {
|
||||
return newState;
|
||||
}
|
||||
if (action.type === RESET_ROLE_CREATE_STATE) {
|
||||
return {
|
||||
...state,
|
||||
[apiMethod.create]: getInitialState()[apiMethod.create],
|
||||
[apiMethod.detail]: getInitialState()[apiMethod.detail],
|
||||
[apiMethod.update]: getInitialState()[apiMethod.update]
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default role;
|
||||
31
src/redux/reducers/ui.js
Normal file
31
src/redux/reducers/ui.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { CLOSE_DRAWER, OPEN_DRAWER } from "../actions";
|
||||
|
||||
const initialState = {
|
||||
drawer: {
|
||||
open: false
|
||||
}
|
||||
};
|
||||
|
||||
const user = (state = initialState, action) => {
|
||||
if (action.type === OPEN_DRAWER) {
|
||||
return {
|
||||
...state,
|
||||
drawer: {
|
||||
...state.drawer,
|
||||
open: true
|
||||
}
|
||||
};
|
||||
} else if (action.type === CLOSE_DRAWER) {
|
||||
return {
|
||||
...state,
|
||||
drawer: {
|
||||
...state.drawer,
|
||||
open: false
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default user;
|
||||
29
src/redux/reducers/ui.test.js
Normal file
29
src/redux/reducers/ui.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import ui from "./ui";
|
||||
import { openDrawer, closeDrawer } from "../actions";
|
||||
import { initialState } from "../../mocks";
|
||||
|
||||
it("Correctly sets the openDrawer value", () => {
|
||||
const previousState = initialState.ui;
|
||||
previousState.drawer.open = false;
|
||||
const actions = [openDrawer()];
|
||||
const nextState = actions.reduce(
|
||||
(prev, action) => ui(prev, action),
|
||||
previousState
|
||||
);
|
||||
expect(nextState.drawer).toEqual({
|
||||
open: true
|
||||
});
|
||||
});
|
||||
|
||||
it("Correctly sets the closeDrawer value", () => {
|
||||
const previousState = initialState.ui;
|
||||
previousState.drawer.open = true;
|
||||
const actions = [closeDrawer()];
|
||||
const nextState = actions.reduce(
|
||||
(prev, action) => ui(prev, action),
|
||||
previousState
|
||||
);
|
||||
expect(nextState.drawer).toEqual({
|
||||
open: false
|
||||
});
|
||||
});
|
||||
24
src/redux/reducers/user.js
Normal file
24
src/redux/reducers/user.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RESET_USER_CREATE_STATE } from "../actions";
|
||||
import { apiReducerTemplate } from "./apiReducer";
|
||||
import { entity, getInitialEntityState } from "../../lib/entity";
|
||||
import { apiMethod } from "../../config";
|
||||
|
||||
const getInitialState = () => getInitialEntityState(entity.user);
|
||||
|
||||
const user = (state = getInitialState(), action) => {
|
||||
let newState = apiReducerTemplate(entity.user)(state, action);
|
||||
if (newState) {
|
||||
return newState;
|
||||
}
|
||||
if (action.type === RESET_USER_CREATE_STATE) {
|
||||
return {
|
||||
...state,
|
||||
[apiMethod.create]: getInitialState()[apiMethod.create],
|
||||
[apiMethod.detail]: getInitialState()[apiMethod.detail],
|
||||
[apiMethod.update]: getInitialState()[apiMethod.update]
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default user;
|
||||
194
src/redux/sagas/index.js
Normal file
194
src/redux/sagas/index.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import { all, put, takeLatest, call } from "redux-saga/effects";
|
||||
import { push } from "react-router-redux";
|
||||
import { get } from "lodash";
|
||||
import apiCreator from "../../lib/api";
|
||||
import config, { apiMethod, apiHttpMethodMapping } from "../../config";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { REHYDRATE } from "redux-persist";
|
||||
import {
|
||||
fetchLoginSuccess,
|
||||
fetchLoginFailure,
|
||||
reduxRehydrationCompleted,
|
||||
fetchGenericSuccess,
|
||||
fetchGenericFailure
|
||||
} from "../actions";
|
||||
import {
|
||||
LOGOUT,
|
||||
FETCH_LOGIN_REQUEST,
|
||||
FETCH_LOGIN_SUCCESS,
|
||||
FETCH_GENERIC_REQUEST,
|
||||
FETCH_GENERIC_SUCCESS
|
||||
} from "../actions";
|
||||
import { takeEvery } from "redux-saga";
|
||||
|
||||
/**
|
||||
* create the default api here. it will be replaced
|
||||
* once the user logs in successfully
|
||||
*/
|
||||
let api = apiCreator.create();
|
||||
|
||||
const handleResponseJsonError = (errorMessage, statusCode) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const error = {
|
||||
error: config.ERROR.UNPARSABLE_RESPONSE
|
||||
};
|
||||
resolve(error);
|
||||
});
|
||||
};
|
||||
|
||||
export function* handleResponse(
|
||||
response,
|
||||
actionSuccess,
|
||||
actionFailure,
|
||||
callIfSuccess,
|
||||
...rest
|
||||
) {
|
||||
let data = yield response.json().catch(handleResponseJsonError);
|
||||
if (response.ok) {
|
||||
//This is temporary.. the api is not responding as expected..
|
||||
if (!data.content && !data.access_token) {
|
||||
data = {
|
||||
content: data
|
||||
};
|
||||
}
|
||||
if (actionSuccess) {
|
||||
yield put(actionSuccess(data));
|
||||
}
|
||||
if (callIfSuccess) {
|
||||
yield call(callIfSuccess, data, ...rest);
|
||||
}
|
||||
} else {
|
||||
if (actionFailure) {
|
||||
yield put(actionFailure(data));
|
||||
}
|
||||
if (response.status === 401) {
|
||||
yield put(push("/login"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchSaga(api, action) {
|
||||
const { payload, method, entity } = action;
|
||||
const { parameters, data } = payload || {};
|
||||
const request = {
|
||||
endpoint: entity.endpoint,
|
||||
parameters,
|
||||
data
|
||||
};
|
||||
if (method === apiMethod.list) {
|
||||
// no todo yet
|
||||
} else if (method === apiMethod.detail) {
|
||||
request.endpoint = `${entity.endpoint}/${data.id}`;
|
||||
} else if (method === apiMethod.create) {
|
||||
// no todo yet
|
||||
} else if (method === apiMethod.update) {
|
||||
request.endpoint = `${entity.endpoint}/${data.id}`;
|
||||
} else if (method === apiMethod.delete) {
|
||||
request.endpoint = `${entity.endpoint}/${data.id}`;
|
||||
} else {
|
||||
console.error("Unrecognized API Action");
|
||||
return;
|
||||
}
|
||||
const response = yield call(api[apiHttpMethodMapping[method]], request);
|
||||
yield call(
|
||||
handleResponse,
|
||||
response,
|
||||
fetchGenericSuccess(method)(entity),
|
||||
fetchGenericFailure(method)(entity)
|
||||
);
|
||||
|
||||
// after delete we need to update state containing all entities of a kind (e.g. all roles, all users)
|
||||
// so we do it manually
|
||||
if (method === apiMethod.delete) {
|
||||
const response = yield call(
|
||||
api[apiHttpMethodMapping[apiMethod.list]],
|
||||
Object.assign({}, request, {
|
||||
endpoint: entity.endpoint
|
||||
})
|
||||
);
|
||||
yield call(
|
||||
handleResponse,
|
||||
response,
|
||||
fetchGenericSuccess(apiMethod.list)(entity),
|
||||
fetchGenericFailure(apiMethod.list)(entity)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchSuccessSaga(action) {
|
||||
const { method, entity } = action;
|
||||
if (method === apiMethod.delete || method === apiMethod.create) {
|
||||
yield put(push(`/${entity.name}/list`));
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchLoginSuccessCallback(data) {
|
||||
const { access_token } = data;
|
||||
let decodedJwt = null;
|
||||
try {
|
||||
decodedJwt = jwtDecode(access_token);
|
||||
} catch (e) {
|
||||
console.error("Error decoding the received JWT Token: ", e);
|
||||
}
|
||||
if (!decodedJwt) {
|
||||
const error = {
|
||||
statusCode: 403,
|
||||
devMessage: "Couldn't decode JWT.",
|
||||
error: config.ERROR.UNAUTHORIZED
|
||||
};
|
||||
yield put(fetchLoginFailure(error));
|
||||
} else {
|
||||
yield put(fetchLoginSuccess({ ...data, decodedJwt }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchLoginSaga(api, action) {
|
||||
const { username, password } = action.payload || {};
|
||||
const request = {
|
||||
endpoint: "login",
|
||||
parameters: null,
|
||||
data: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
};
|
||||
const response = yield call(api.post, request);
|
||||
yield call(
|
||||
handleResponse,
|
||||
response,
|
||||
null,
|
||||
fetchLoginFailure,
|
||||
fetchLoginSuccessCallback
|
||||
);
|
||||
}
|
||||
|
||||
export function* handleSuccessfulLogin(action) {
|
||||
const { data: { access_token } } = action.payload;
|
||||
api.setToken(access_token);
|
||||
//redirect the user to the main page
|
||||
yield put(push("/role/create"));
|
||||
}
|
||||
|
||||
export function* reduxRehydrateSaga(api, action) {
|
||||
const { payload = {} } = action;
|
||||
const access_token = get(payload, "login.access_token");
|
||||
if (!api.getToken() && access_token) {
|
||||
api.setToken(access_token);
|
||||
}
|
||||
yield put(reduxRehydrationCompleted());
|
||||
}
|
||||
|
||||
export function* handleLogout(action) {
|
||||
yield put(push("/login"));
|
||||
}
|
||||
|
||||
export default function* root() {
|
||||
yield all([
|
||||
takeLatest(REHYDRATE, reduxRehydrateSaga, api),
|
||||
takeEvery(FETCH_GENERIC_REQUEST, fetchSaga, api),
|
||||
takeEvery(FETCH_GENERIC_SUCCESS, fetchSuccessSaga),
|
||||
takeLatest(FETCH_LOGIN_REQUEST, fetchLoginSaga, api),
|
||||
takeLatest(FETCH_LOGIN_SUCCESS, handleSuccessfulLogin),
|
||||
takeLatest(LOGOUT, handleLogout)
|
||||
]);
|
||||
}
|
||||
17
src/redux/selectors/contactsSelectors.js
Normal file
17
src/redux/selectors/contactsSelectors.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
getItems,
|
||||
getItemById,
|
||||
getIsFetching,
|
||||
getTimeFetched
|
||||
} from "./entitySelectors";
|
||||
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 isFetchingContactList = getIsFetching(apiMethod.list)(
|
||||
entity.contact
|
||||
);
|
||||
export const getTimeFetchedContactList = getTimeFetched(apiMethod.list)(
|
||||
entity.contact
|
||||
);
|
||||
24
src/redux/selectors/entitySelectors.js
Normal file
24
src/redux/selectors/entitySelectors.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { find, get } from "lodash";
|
||||
|
||||
export const getEntity = ({ name }) => state => state[name];
|
||||
export const getEntityMethod = method => entity => state =>
|
||||
(getEntity(entity)(state) || {})[method];
|
||||
export const getEntityMethodValue = key => method => entity => state =>
|
||||
(getEntityMethod(method)(entity)(state) || {})[key];
|
||||
export const getEntityItems = entity => state =>
|
||||
get(getEntity(entity)(state), "items") || [];
|
||||
// Use preferably the ones below to create selectors.
|
||||
export const getItems = method => entity => state => {
|
||||
const allItems = getEntityItems(entity)(state);
|
||||
const itemIds = getEntityMethodValue("items")(method)(entity)(state) || [];
|
||||
return allItems.filter(item => itemIds.includes(item.id));
|
||||
};
|
||||
export const getMeta = getEntityMethodValue("meta");
|
||||
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);
|
||||
};
|
||||
11
src/redux/selectors/formSelectors.js
Normal file
11
src/redux/selectors/formSelectors.js
Normal file
@@ -0,0 +1,11 @@
|
||||
//Form values
|
||||
export const getForm = (formName = "default") => state => state.form[formName];
|
||||
export const getFormValues = formName => state =>
|
||||
(getForm(formName)(state) || {}).values;
|
||||
export const getLoginFormValues = state => getFormValues("login")(state);
|
||||
export const getUserFormValues = state =>
|
||||
getFormValues("UserCreateForm")(state);
|
||||
export const getGroupFormValues = state =>
|
||||
getFormValues("GroupCreateForm")(state);
|
||||
export const getRoleFormValues = state =>
|
||||
getFormValues("RoleCreateForm")(state);
|
||||
32
src/redux/selectors/groupSelectors.js
Normal file
32
src/redux/selectors/groupSelectors.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { get } from "lodash";
|
||||
import { apiMethod } from "../../config";
|
||||
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 isLoadedGroups = state =>
|
||||
!get(state, "group.list.isFetching", false) &&
|
||||
get(state, "group.list.timeFetched", true);
|
||||
|
||||
export const isLoadedGroup = state =>
|
||||
!get(state, "group.detail.isFetching", false) &&
|
||||
get(state, "group.detail.timeFetched", true);
|
||||
|
||||
export const isCreatingGroup = state =>
|
||||
get(state, "group.create.isFetching", false);
|
||||
|
||||
export const getCreateGroupTimeFetched = state =>
|
||||
get(state, "group.create.timeFetched", null);
|
||||
|
||||
export const getCreateGroupError = state =>
|
||||
get(state, "group.create.error", null);
|
||||
|
||||
export const isEditedGroup = state =>
|
||||
get(state, "group.update.timeFetched", false) &&
|
||||
!get(state, "group.update.error", false);
|
||||
|
||||
export const isGroupWithErrors = state =>
|
||||
get(state, "group.list.error", null) ||
|
||||
get(state, "group.detail.error", null);
|
||||
10
src/redux/selectors/index.js
Normal file
10
src/redux/selectors/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
//login
|
||||
|
||||
export * from "./entitySelectors";
|
||||
export * from "./loginSelectors";
|
||||
export * from "./formSelectors";
|
||||
export * from "./uiSelectors";
|
||||
export * from "./permissionSelectors";
|
||||
export * from "./roleSelectors";
|
||||
export * from "./userSelectors";
|
||||
export * from "./groupSelectors";
|
||||
2
src/redux/selectors/loginSelectors.js
Normal file
2
src/redux/selectors/loginSelectors.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const getLogin = (key = "isLoggedIn") => state => state.login[key];
|
||||
export const getLoginError = state => getLogin("error")(state);
|
||||
13
src/redux/selectors/permissionSelectors.js
Normal file
13
src/redux/selectors/permissionSelectors.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { get } from "lodash";
|
||||
import { apiMethod } from "../../config";
|
||||
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 isFetchingPermissions = state =>
|
||||
!!get(state, `${entity.permission.name}.list.isFetching`);
|
||||
|
||||
export const getTimeFetchedPermissions = state =>
|
||||
get(state, `${entity.permission.name}.list.timeFetched`);
|
||||
33
src/redux/selectors/roleSelectors.js
Normal file
33
src/redux/selectors/roleSelectors.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { get } from "lodash";
|
||||
import { apiMethod } from "../../config";
|
||||
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 getCreateRole = state => get(state, "role.create");
|
||||
|
||||
export const isLoadedRoles = state =>
|
||||
!get(state, "role.list.isFetching", false) &&
|
||||
get(state, "role.list.timeFetched", true);
|
||||
|
||||
export const isLoadedRole = state =>
|
||||
!get(state, "role.detail.isFetching", false) &&
|
||||
get(state, "role.detail.timeFetched", true);
|
||||
|
||||
export const isCreatingRole = state =>
|
||||
get(state, "role.create.isFetching", false);
|
||||
|
||||
export const isEditedRole = state =>
|
||||
get(state, "role.update.timeFetched", false) &&
|
||||
!get(state, "role.update.error", false);
|
||||
|
||||
export const getCreateRoleTimeFetched = state =>
|
||||
get(state, "role.create.timeFetched", null);
|
||||
|
||||
export const getCreateRoleError = state =>
|
||||
get(state, "role.create.error", null);
|
||||
|
||||
export const isRoleWithErrors = state =>
|
||||
get(state, "role.list.error", null) || get(state, "role.detail.error", null);
|
||||
7
src/redux/selectors/uiSelectors.js
Normal file
7
src/redux/selectors/uiSelectors.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const getDrawer = state => state.ui.drawer;
|
||||
|
||||
export const isDrawerOpen = state => {
|
||||
const drawer = getDrawer(state);
|
||||
if (!drawer) return false;
|
||||
return !!drawer.open;
|
||||
};
|
||||
29
src/redux/selectors/uiSelectors.test.js
Normal file
29
src/redux/selectors/uiSelectors.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getDrawer, isDrawerOpen } from "./uiSelectors";
|
||||
|
||||
const state = {
|
||||
ui: {
|
||||
drawer: {
|
||||
open: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const state2 = {
|
||||
ui: {}
|
||||
};
|
||||
|
||||
it("getDrawer returns the correct part of the state", () => {
|
||||
const drawer = getDrawer(state);
|
||||
expect(drawer).toEqual({
|
||||
open: true
|
||||
});
|
||||
const drawer2 = getDrawer(state2);
|
||||
expect(drawer2).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("isDrawerOpen returns the correct value", () => {
|
||||
const drawer = isDrawerOpen(state);
|
||||
expect(drawer).toEqual(true);
|
||||
const drawer2 = isDrawerOpen(state2);
|
||||
expect(drawer2).toEqual(false);
|
||||
});
|
||||
44
src/redux/selectors/userSelectors.js
Normal file
44
src/redux/selectors/userSelectors.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
getItems,
|
||||
getItemById,
|
||||
getIsFetching,
|
||||
getTimeFetched
|
||||
} from "./entitySelectors";
|
||||
import { entity } from "../../lib/entity";
|
||||
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 isFetchingUserList = getIsFetching(apiMethod.list)(entity.user);
|
||||
export const getTimeFetchedUserList = getTimeFetched(apiMethod.list)(
|
||||
entity.user
|
||||
);
|
||||
|
||||
export const isCreatingUser = state =>
|
||||
get(state, "user.create.isFetching", false);
|
||||
|
||||
export const isEditedUser = state => {
|
||||
return (
|
||||
get(state, "user.update.timeFetched", false) &&
|
||||
!get(state, "user.update.error", false)
|
||||
);
|
||||
};
|
||||
|
||||
export const getCreateUserError = state =>
|
||||
get(state, "user.create.error", null);
|
||||
|
||||
export const isUserWithErrors = state =>
|
||||
get(state, "user.list.error", null) || get(state, "user.detail.error", null);
|
||||
|
||||
export const isLoadedUsers = state =>
|
||||
!get(state, "user.list.isFetching", false) &&
|
||||
get(state, "user.list.timeFetched", true);
|
||||
|
||||
export const isLoadedUser = state =>
|
||||
!get(state, "user.detail.isFetching", false) &&
|
||||
get(state, "user.detail.timeFetched", true);
|
||||
|
||||
//keep for legacy code
|
||||
export const isFetchingUsers = isFetchingUserList;
|
||||
export const getTimeFetchedUsers = getTimeFetchedUserList;
|
||||
108
src/registerServiceWorker.js
Normal file
108
src/registerServiceWorker.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// In production, we register a service worker to serve assets from local cache.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||
// cached resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === "localhost" ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === "[::1]" ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export default function register() {
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === "installed") {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log("New content is available; please refresh.");
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log("Content is cached for offline use.");
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get("content-type").indexOf("javascript") === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
4
src/setupTests.js
Normal file
4
src/setupTests.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import enzyme from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
enzyme.configure({ adapter: new Adapter() });
|
||||
3
src/version.js
Normal file
3
src/version.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
version: "0.5.0"
|
||||
};
|
||||
Reference in New Issue
Block a user