finish new responsive contact form

This commit is contained in:
Leonard Krause
2018-04-27 08:09:55 +02:00
parent 5b6de60cfa
commit 8ca8ee061f
10 changed files with 186 additions and 41 deletions

View File

@@ -66,11 +66,6 @@ class App extends Component {
path="/contact/:id/:type"
component={ContactDetailScreen}
/>
<Route
exact
path="/contact/:id/:type"
component={ContactDetailScreen}
/>
<Route
exact
path="/contact/:type"

View File

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

View File

@@ -1,7 +1,13 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { withStyles, Card, CardContent, Typography } from "material-ui";
import {
withStyles,
Card,
CardContent,
Typography,
CircularProgress
} from "material-ui";
import PropTypes from "prop-types";
import { initialize } from "redux-form";
import { get } from "lodash";
@@ -10,18 +16,34 @@ import { goBack } from "react-router-redux";
import {
getContactById,
getEntityItems,
getContactFormValues
getContactFormValues,
getIsFetching
} from "../../redux/selectors";
import { entity } from "../../lib/entity";
import { detailScreenType } from "../../config";
import { detailScreenType, apiMethod } from "../../config";
import Screen from "../Screen";
import ContactForm from "./ContactForm";
import { fetchUpdateByObject, fetchCreateByObject } from "../../redux/actions";
import {
fetchUpdateByObject,
fetchCreateByObject,
fetchDetailById,
fetchAll
} from "../../redux/actions";
const styles = theme => ({
progress: {
margin: theme.spacing.unit * 2
},
progressContainer: {
width: "100%",
flex: 1,
display: "flex",
flexDirection: "row",
justifyContent: "center"
},
card: {
minWidth: 275
height: "100%"
},
bullet: {
display: "inline-block",
@@ -38,15 +60,59 @@ const styles = theme => ({
});
export class DetailScreen extends Component {
state = {
multiGroups: []
};
componentWillMount = () => {
const { fetchAllGroups } = this.props;
this.fetchContact();
fetchAllGroups();
};
fetchContact = () => {
const { fetchContactDetailById, type, id } = this.props;
if (type === detailScreenType.edit || type === detailScreenType.view) {
if (!!id || id === 0) {
fetchContactDetailById(id);
}
}
};
//TODO: fix the React Select and make it a proper redux forms field
handleChangeMultiGroups = multiGroups => {
this.setState({
multiGroups: !!multiGroups
? multiGroups.split(",").map(v => parseInt(v, 10))
: []
});
};
componentWillReceiveProps = nextProps => {
const { contact } = this.props;
if (contact !== nextProps.contact && nextProps.contact) {
this.setState({
multiGroups: nextProps.contact.groups
});
}
};
handleSubmit = values => {
const { multiGroups } = this.state;
const { type, fetchUpdateByObject, fetchCreateByObject } = this.props;
if (type === detailScreenType.edit) {
fetchUpdateByObject(values);
fetchUpdateByObject({
...values,
groups: multiGroups
});
return true;
}
if (type === detailScreenType.create) {
fetchCreateByObject(values);
fetchCreateByObject({
...values,
groups: multiGroups
});
return true;
}
return false;
@@ -59,7 +125,22 @@ export class DetailScreen extends Component {
};
render = () => {
const { classes, t, contact, name, type } = this.props;
const { multiGroups } = this.state;
console.log(multiGroups);
const { classes, t, contact, name, type, groups, isLoading } = this.props;
const optionsGroups = (groups || []).map(group => ({
value: group.id,
label: group.name
}));
if (isLoading) {
return (
<Screen>
<div className={classes.progressContainer}>
<CircularProgress className={classes.progress} />
</div>
</Screen>
);
}
return (
<Screen>
<div>
@@ -78,6 +159,9 @@ export class DetailScreen extends Component {
enableReinitialize={true}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
optionsGroups={optionsGroups}
multiGroups={multiGroups}
handleChangeMultiGroups={this.handleChangeMultiGroups}
/>
</Card>
</div>
@@ -92,9 +176,15 @@ const mapStateToProps = (state, ownProps) => {
const contact = getContactById(id)(state);
const groups = getEntityItems(entity.group)(state);
const isFetchingContact = getIsFetching(apiMethod.detail)(entity.contact)(
state
);
const isFetchingGroups = getIsFetching(apiMethod.all)(entity.group)(state);
const values = getContactFormValues(state);
return {
isLoading: isFetchingContact || isFetchingGroups,
id,
type,
contact,
@@ -112,7 +202,9 @@ const mapDispatchToProps = {
initialize,
goBack,
fetchCreateByObject: fetchCreateByObject(entity.contact),
fetchUpdateByObject: fetchUpdateByObject(entity.contact)
fetchUpdateByObject: fetchUpdateByObject(entity.contact),
fetchContactDetailById: fetchDetailById(entity.contact),
fetchAllGroups: fetchAll(entity.group)
};
DetailScreen.propTypes = {

View File

@@ -6,6 +6,7 @@ import { reduxForm, Form } from "redux-form";
import { translate } from "react-i18next";
import { FormTextField, formValidations } from "../layout/FormFields";
import ReactSelect from "../ReactSelect";
const styles = theme => ({
form: {
@@ -16,6 +17,9 @@ const styles = theme => ({
},
actions: {
width: "100%"
},
formItem: {
width: "100%"
}
});
@@ -24,20 +28,17 @@ export class ContactForm extends Component {
const {
handleSubmit,
onCancel,
pristine,
invalid,
submitting,
asyncValidating,
classes,
t
t,
optionsGroups = [],
handleChangeMultiGroups,
multiGroups
} = this.props;
console.log(this.props);
const canSubmit = !(
pristine ||
submitting ||
invalid ||
asyncValidating === true
);
const canSubmit = !(submitting || invalid || asyncValidating === true);
return (
<Form onSubmit={handleSubmit} className={classes.form}>
<CardContent>
@@ -82,6 +83,18 @@ export class ContactForm extends Component {
name="bankDetails"
/>
</Grid>
<Grid item xs={12} md={6}>
{/*TODO: fix the reactselect component to work with redux-forms.*/}
<ReactSelect
className={classes.formItem}
name={"selectGroups"}
options={optionsGroups}
handleChangeMulti={handleChangeMultiGroups}
multi={multiGroups}
canAddMultipleValues={true}
placeholder={t("contact.placeholder.groups")}
/>
</Grid>
</Grid>
</CardContent>
<CardActions className={classes.actions}>
@@ -115,7 +128,8 @@ ContactForm.propTypes = {
classes: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool
submitting: PropTypes.bool,
groups: PropTypes.array
};
export default reduxForm({ form: "contact" })(

View File

@@ -1,5 +1,6 @@
export const apiMethod = {
list: "list",
all: "all",
detail: "detail",
create: "create",
update: "update",
@@ -14,6 +15,7 @@ export const detailScreenType = {
export const apiHttpMethodMapping = {
list: "get",
all: "get",
detail: "get",
create: "post",
update: "put",
@@ -34,6 +36,9 @@ export const config = {
ERROR: {
NOCONNECTION: "NOCONNECTION",
UNAUTHORIZED: "UNAUTHORIZED"
},
CONSTANTS: {
FETCH_ALL_SIZE: 100
}
};

View File

@@ -32,7 +32,8 @@ export default {
},
saveButtonLabel: "Save",
title: {
create: "Create"
create: "Create",
edit: "Edit"
}
},
label: {
@@ -43,6 +44,9 @@ export default {
address: "Address",
bankDetails: "Bank Details",
groups: "Groups"
},
placeholder: {
groups: "Groups"
}
},
user_create_screen: {

View File

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

View File

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

View File

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

View File

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