I am pretty new to react on rails, I am sorry if this is something obvious! I know there are pages of this question, but none have fixed the error I am getting. Any advice is greatly appreciated!
When I submit a form from my assets Component my rails rails server returns-
ActionController::ParameterMissing (param is missing or the value is empty: asset):
app/controllers/api/assets_controller.rb:48:in `asset_params'
app/controllers/api/assets_controller.rb:14:in `create'
Route.rb
Rails.application.routes.draw do
mount_devise_token_auth_for 'User', at: 'api/auth'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
namespace :api do
resources :areas do
resources :assets
end
end
end
model
class Asset < ApplicationRecord
belongs_to :area
validates :name, :description, :barcode, :price, :pdate, :status, :img, :category, presence: true
end
assets_controller
class Api::AssetsController < ApplicationController
before_action :set_area
before_action :set_asset, only: [:show, :update, :destroy]
def index
render json: #area.assets
end
def show
render json: #asset
end
def create
#asset = #area.assets.new(asset_params)
if #asset.save
render json: #asset
else
errKey = #asset.errors.messages.keys[0].to_s
errValue = #asset.errors.messages.values[0][0]
render json: {errors: "#{errKey} #{errValue}"}, status: :unprocessable_entity
end
end
def update
if #asset.update(asset_params)
render json:#asset
else
render json: {errors: #asset.errors }, status: :unprocessable_entity
end
end
def destroy
#asset.destroy
render json: {message: "Asset Deleted"}
end
private
def set_area
#area = Area.find(params[:area_id])
end
def set_asset
#asset = #area.assets.find(params[:id])
end
def asset_params
params.require(:asset).permit(:name, :description, :barcode, :price, :pdate, :status, :img, :category)
# params.fetch(:asset).permit(:name :description, :barcode, :price, :pdate, :status, :img, :category)
end
end
###Note when I use params.fetch I still get the same error
AssetProvider.js
import React, {useState } from 'react';
import axios from 'axios';
import {useNavigate} from 'react-router-dom';
export const AssetContext = React.createContext();
export const AssetConsumer = AssetContext.Consumer;
const AssetProvider = ({ children }) => {
const [assets, setAssets] = useState([])
const [errors, setErrors] = useState([])
const getAllAssets = (areaId) => {
axios.get(`/api/areas/${areaId}/assets`)
.then (res => setAssets(res.data))
.catch(err => {
setErrors({
variant: 'danger',
msg: err.response.data.errors.full_messages[0]
})
})
}
const addAsset = (areaId, asset) => {
axios.post(`/api/areas/${areaId}/assets`, { asset })
.then ( res => setAssets([...assets, res.data]))
.catch(err => {
setErrors({
variant: 'danger',
msg: Object.keys(err.response.data.errors)[0] + " " + Object.values(err.response.data.errors)[0][0]
})
})
}
const updateAsset = (areaId, id, asset) => {
axios.put (`/api/areas/${areaId}/assets/${id}`, {asset})
.then ( res => {
const newUpdatedAssets = assets.map(a => {
if (a.id === id) {
return res.data
}
return a
})
setAssets(newUpdatedAssets)
})
.catch(err => {
setErrors({
variant: 'danger',
msg: Object.keys(err.response.data.errors)[0] + " " + Object.values(err.response.data.errors)[0][0]
})
})
}
const deleteAsset = (areaId, id) => {
axios.delete(`/api/areas/${areaId}/assets/${id}`)
.then (res => {
setAssets(assets.filter(a => a.id !== id))
})
.catch(err => {
setErrors({
variant: 'danger',
msg: err.response.data.errors[0]
})
})
}
return (
<AssetContext.Provider value={{
assets,
errors,
setErrors,
getAllAssets,
addAsset,
updateAsset,
deleteAsset,
}}>
{children}
</AssetContext.Provider>
)
}
export default AssetProvider;
AssetForm.js
import { useState} from 'react'
import {Form, Button} from 'react-bootstrap'
import {AssetConsumer} from '../../providers/AssetProvider'
import { useParams} from 'react-router-dom'
const AssetForm = ({ setAdd, addAsset}) => {
const [asset, setAsset] = useState ({ name: '', img: '', barcode: '', description: '', category: '', price: '', pdate: '', status: ''})
const { areaId } = useParams();
const handleSubmit = (e) => {
e.preventDefault()
addAsset(areaId, asset)
setAdd(false)
setAsset({ name: '', img: '', barcode: '', description: '', category: '', price: '', pdate: '', status: ''})
}
return (
<>
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" >
<Form.Label>Asset Image</Form.Label>
<Form.Control
name='img'
value={asset.img}
onChange={(e) => setAsset({...asset, img: e.targetvalue})}
/>
</Form.Group>
<Form.Group className="mb-3" >
<Form.Label>Name</Form.Label>
<Form.Control
name='name'
value={asset.name}
onChange={(e) => setAsset({...asset, name: e.targetvalue})}
autoFocus
required
/>
</Form.Group>
<Form.Group className="mb-3" >
<Form.Label>Barcode</Form.Label>
<Form.Control
name='barcode'
value={asset.barcode}
onChange={(e) => setAsset({...asset, barcode: e.targetvalue})}
/>
</Form.Group>
<Form.Group className="mb-3" >
<Form.Label>Description</Form.Label>
<Form.Control
name='description'
value={asset.description}
onChange={(e) => setAsset({...asset, description: e.targetvalue})}
as="textarea"
rows={3}
/>
</Form.Group>
<Form.Group className="mb-3" >
<Form.Label>Category</Form.Label>
<Form.Select name='category'
value={asset.category}
onChange={(e) => setAsset({...asset, category: e.targetvalue})}
>
<option>Select from list</option>
<option value="camera">Camera</option>
<option value="light">Light</option>
<option value="tripod">Tripod</option>
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" >
<Form.Label>Price</Form.Label>
<Form.Control
name='price'
value={asset.price}
onChange={(e) => setAsset({...asset, price: e.targetvalue})}
/>
</Form.Group>
<Form.Group className="mb-3" >
<Form.Label>Purchase Date</Form.Label>
<Form.Control
name='pdate'
value={asset.pdate}
onChange={(e) => setAsset({...asset, pdate: e.targetvalue})}
/>
</Form.Group>
<Form.Group className="mb-3" >
<Form.Label>Status</Form.Label>
<Form.Control
name='status'
value={asset.status}
onChange={(e) => setAsset({...asset, status: e.targetvalue})}
/>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</>
)
}
const ConnectedAssetForm = (props) => (
<AssetConsumer>
{ value => <AssetForm {...value} {...props} />}
</AssetConsumer>
)
export default ConnectedAssetForm;
Schema.rb
create_table "areas", force: :cascade do |t|
t.string "name"
t.string "address"
t.string "city"
t.string "country"
t.integer "zip"
t.string "mcontact"
t.string "pic"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "assets", force: :cascade do |t|
t.string "name"
t.string "description"
t.string "barcode"
t.decimal "price"
t.datetime "pdate"
t.string "status"
t.string "img"
t.bigint "area_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "category"
t.index ["area_id"], name: "index_assets_on_area_id"
end
enter image description here- Example of State being undefined while filling out the form
enter image description here- Example of network payload on form submit.
I have tried adding the :id to the assets model and controller but I still get the same error on my rails server.
I have also tried in the form doing the following for example
<Form.Group className="mb-3" >
<Form.Label>Asset Image</Form.Label>
<Form.Control
name='asset[img]'
value={asset.img}
onChange={(e) => setAsset({...asset, img: e.targetvalue})}
/>
</Form.Group>
but it still returns ActionController: :ParameterMissina (param is missing or the value is empty: asset) :
Thank you!
Related
I am working with GraphQL and React and I have next post form code:
const PostForm = props => {
// set the default state of the form
const [values, setValues] = useState();
// update the state when a user types in the form
const onChange = event => {
setValues({
...values,
[event.target.name]: event.target.value
});
};
return (
<Wrapper>
<Form
onSubmit={event => {
event.preventDefault();
props.action({
variables: {
...values
}
});
}}
>
<label htmlFor="title">Title Post:</label>
<input
required
type="text"
id="title"
name="title"
placeholder="title"
onChange={onChange}
/>
<label htmlFor="category">Category Post:</label>
<input
required
type="text"
id="category"
name="category"
placeholder="category"
onChange={onChange}
/>
<TextArea
required
type="text"
name="body"
placeholder="Post content"
onChange={onChange}
/>
<Button type="submit">Save</Button>
</Form>
</Wrapper>
);
};
This code I have in the new post page:
const NEW_POST = gql`
mutation createPost($title: String, $category: String, $body: String) {
createPost(title: $title, category: $category, body: $body) {
_id
title
createdAt
updatedAt
body
author {
name
}
comments{
text
}
}
}`;
const NewPost = props => {
useEffect(() => {
document.title = 'NewPost - Notedly';
});
const [ data, { loading, error } ] = useMutation(NEW_POST, {
onCompleted: data => {
props.history.push(`posts/${data.createPost._id}`);
}
});
return (
<React.Fragment>
{loading && <p> loading...</p>}
{error && <p>Error saving the note</p>}
{console.log(data)}
<PostForm action={data} />
</React.Fragment>
);
};
I have the following mutation code, for example:
mutation{
createPost(title: "my jobs", category: "6251ef28413373118838bbdd", body: "smdbsdfsjns"){
_id
title
category
{catname}
body
}
}
I don't understand why I am getting this error:
"Uncaught (in promise) Error: Network error: Response not successful: Received status code 400"
When I send a post request without an image everything works ok. When I add an image it seems to fall through. I get an Error: Request failed with status code 409. This is the code for my react form page.
const Form = ({ currentId, setCurrentId }) => {
const [postData, setPostData] = useState({
creator: '', title: '', message: '', tags:'', selectedFiles:''
})
const post = useSelector((state) => currentId ? state.posts.find((p) => p._id === currentId) : null);
const classes = useStyles();
const dispatch = useDispatch();
useEffect(() => {
if(post) setPostData(post);
}, [post])
const handleSubmit = (e) => {
e.preventDefault();
if(currentId) {
dispatch(updatePost(currentId, postData));
} else {
dispatch(createPost(postData));
}
//clear();
}
const clear = () => {
setCurrentId(0);
setPostData({creator: '', title: '', message: '', tags:'', selectedFiles:''})
}
return (
<Paper className={classes.paper}>
<form autoComplete='off' noValidate className={`${classes.root}${classes.form}`} onSubmit={handleSubmit}>
<Typography variant='h6'>{currentId ? 'Editing' : 'Creating' } a Store</Typography>
<TextField name='creator' variant='outlined' label='Creator' fullWidth value={postData.creator}onChange={(e) => setPostData({ ...postData, creator: e.target.value })}/>
<TextField
name='Store Name'
variant='outlined'
label='name'
fullWidth
value={postData.title}
onChange={(e) => setPostData({ ...postData, title: e.target.value })}
/>
<TextField
name='message'
variant='outlined'
label='message'
fullWidth
value={postData.message}
onChange={(e) => setPostData({ ...postData, message: e.target.value })}
/>
<TextField
name='crypto'
variant='outlined'
label='crypto'
fullWidth
value={postData.tags}
onChange={(e) => setPostData({ ...postData, tags: e.target.value })}
/>
<div className={classes.fileInput}>
<FileBase
type='file'
multiple={false}
onDone={(base64) => setPostData({ ...postData, selectedFile: base64})}
/>
</div>
<Button className={classes.buttonSubmit} variant="container" color="primary" size="large" type="submit" fullwidth>Submit</Button>
<Button variant="contained" color="secondary" size="small" onClick={clear} fullwidth>Clear</Button>
</form>
</Paper>
);
}
export default Form;
This is the function for my server side route. WHere I take the form info and post it to the server.
export const createPost = async (req, res) => {
const { title, message, selectedFile, creator, tags } = req.body;
const newPostMessage = new PostMessage({ title, message, selectedFile, creator, tags })
try {
await newPostMessage.save();
res.status(201).json(newPostMessage );
} catch (error) {
res.status(409).json({ message: error.message });
}
}
This is my model for mongodb.
import mongoose from 'mongoose';
const postSchema = mongoose.Schema({
title: String,
message: String,
creator: String,
tags: [String],
selectedFile: String,
likeCount: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: new Date()
},
})
const PostMessage = mongoose.model('PostMessage', postSchema);
export default PostMessage;
Your Mongo model has selectedFile declared as a String. Your frontend is sending a base64-encoded jpeg file, which Mongo doesn't know how to convert into a String.
Check out this question for some leads on storing jpegs in Mongo.
In my case, the problem was on this line:
<FileBase
type='file'
multiple={false}
onDone={(base64) => setPostData({ ...postData, selectedFile: base64})}
/>
the solution :
<FileBase
type="file"
multiple={false}
onDone={({ base64 }) =>
setPostData({ ...postData, selectedFile: base64 })
}
/>
The only difference is the {}.
Trying to change the isVegan object (nested boolean) with React Bootstrap checkbox and hooks. I can access the object without any issues (e.g. checkbox is checked if isVegan is true), but have been unable to modify the state. As you can see in the Redux dev tools (image link included), the isVegan object is passed through my state and is accessible. I have also used similar code for the other objects in the chef collection without any issues so believe the issue is either related to the checkbox or how the isVegan object is nested in the chef collection. (Lastly, I know some of the code below may be extra, I slimmed down my original file to simplify this example)
import React, { useState, useEffect, setState } from 'react';
import { Form, Button, Row, Col, Tabs, Tab } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { getChefDetails, updateChefProfile } from '../../actions/chefActions';
import { CHEF_UPDATE_PROFILE_RESET } from '../../constants/chefConstants';
import FormContainer from '../../components/FormContainer/FormContainer.component';
import './ProfileEditPage.styles.scss';
const ProfileEditPage = ({ location, history }) => {
const [first_name, setFirstName] = useState('')
const [last_name, setLastName] = useState('')
const [username, setUsername] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [isVegan, setIsVegan] = useState('')
const [bio, setBio] = useState('')
const [message, setMessage] = useState(null)
const dispatch = useDispatch()
const chefDetails = useSelector(state => state.chefDetails)
const { loading, error, chef } = chefDetails
const chefLogin = useSelector(state => state.chefLogin)
const { chefInfo } = chefLogin
const chefUpdateProfile = useSelector(state => state.chefUpdateProfile)
const { success } = chefUpdateProfile
useEffect(() => {
if(!chefInfo) {
history.push('/login')
} else {
if(!chef || !chef.username || success) {
dispatch({ type: CHEF_UPDATE_PROFILE_RESET })
dispatch(getChefDetails('profile'))
} else {
setFirstName(chef.first_name)
setLastName(chef.last_name)
setUsername(chef.username)
setEmail(chef.email)
setBio(chef.bio)
setIsVegan(chef.isVegan)
}
}
}, [dispatch, history, chefInfo, chef, success])
const submitHandler = (e) => {
e.preventDefault()
if (password !== confirmPassword) {
setMessage('Passwords do not match')
} else {
dispatch(updateChefProfile({
id: chef._id,
first_name,
last_name,
username,
email,
password,
bio,
isVegan
}))
}
}
const [key, setKey] = useState('auth')
//const isVegan = chef.diets[0].isVegan
//const isVegetarian = chef.diets[0].isVegetarian
console.log(isVegan)
return (
<FormContainer className="profileEditPage">
<h1>Chef Profile</h1>
<Form className='profileEditPageForm' onSubmit={submitHandler}>
<Tabs id="profileEditPageTabs" activeKey={key} onSelect={(k) => setKey(k)}>
<Tab eventKey='auth' title="Auth">
<Form.Group controlId='first_name'>
<Form.Label>First Name</Form.Label>
<Form.Control
type='text'
placeholder='Enter your first name'
value={first_name}
onChange={(e) => setFirstName(e.target.value)}
required
>
</Form.Control>
</Form.Group>
<Form.Group controlId='last_name'>
<Form.Label>Last Name</Form.Label>
<Form.Control
type='text'
placeholder='Enter your last name'
value={last_name}
onChange={(e) => setLastName(e.target.value)}
required
>
</Form.Control>
</Form.Group>
<Form.Group controlId='username'>
<Form.Label>Username</Form.Label>
<Form.Control
type='text'
placeholder='Enter a username'
value={username}
onChange={(e) => setUsername(e.target.value)}
required
>
</Form.Control>
<Form.Text className='muted'>Your username will be public</Form.Text>
</Form.Group>
<Form.Group controlId='email'>
<Form.Label>Email</Form.Label>
<Form.Control
type='email'
placeholder='Enter your email'
value={email}
onChange={(e) => setEmail(e.target.value)}
required
>
</Form.Control>
</Form.Group>
<Form.Group controlId='password'>
<Form.Label>Password</Form.Label>
<Form.Control
type='password'
placeholder='Enter your password'
value={password}
onChange={(e) => setPassword(e.target.value)}
>
</Form.Control>
</Form.Group>
<Form.Group controlId='confirmPassword'>
<Form.Label>Confirm Password</Form.Label>
<Form.Control
type='password'
placeholder='Confirm password'
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
>
</Form.Control>
</Form.Group>
</Tab>
<Tab eventKey='chef-detail' title="Chef Detail">
<Form.Group controlId='isVegan'>
<Form.Check
type='checkbox'
label='Vegan?'
checked={isVegan}
value={isVegan}
onChange={(e) => setIsVegan(e.target.checked)}
/>
</Form.Group>
<Form.Group controlId='bio'>
<Form.Label>Chef Bio</Form.Label>
<Form.Control
as='textarea'
rows='5'
maxLength='240'
placeholder='Enter bio'
value={bio}
onChange={(e) => setBio(e.target.value)}
>
</Form.Control>
<Form.Text className='muted'>Your bio will be public</Form.Text>
</Form.Group>
</Tab>
</Tabs>
<Button type='submit' variant='primary'>
Update
</Button>
</Form>
</FormContainer>
)
}
export default ProfileEditPage;
Actions
export const getChefDetails = (id) => async (dispatch, getState) => {
try {
dispatch({
type: CHEF_DETAILS_REQUEST
})
const { chefLogin: { chefInfo} } = getState()
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${chefInfo.token}`
}
}
const { data } = await axios.get(
`/api/chefs/${id}`,
config
)
dispatch({
type: CHEF_DETAILS_SUCCESS,
payload: data
})
} catch (error) {
dispatch({
type: CHEF_DETAILS_FAILURE,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
})
}
}
export const updateChefProfile = (chef) => async (dispatch, getState) => {
try {
dispatch({
type: CHEF_UPDATE_PROFILE_REQUEST
})
const { chefLogin: { chefInfo } } = getState()
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${chefInfo.token}`
}
}
const { data } = await axios.put(
`/api/chefs/profile`,
chef,
config
)
dispatch({
type: CHEF_UPDATE_PROFILE_SUCCESS,
payload: data
})
dispatch({
type: CHEF_LOGIN_SUCCESS,
payload: data
})
localStorage.setItem('chefInfo', JSON.stringify(data))
} catch (error) {
dispatch({
type: CHEF_UPDATE_PROFILE_FAILURE,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
})
}
}
Reducers
export const chefDetailsReducer = (state = { chef: { } }, action) => {
switch(action.type) {
case CHEF_DETAILS_REQUEST:
return { ...state, loading: true }
case CHEF_DETAILS_SUCCESS:
return { loading: false, chef: action.payload }
case CHEF_DETAILS_FAILURE:
return { loading: false, error: action.payload }
case CHEF_DETAILS_RESET:
return {
chef: {}
}
default:
return state
}
}
export const chefUpdateProfileReducer = (state = { }, action) => {
switch(action.type) {
case CHEF_UPDATE_PROFILE_REQUEST:
return { loading: true }
case CHEF_UPDATE_PROFILE_SUCCESS:
return { loading: false, success: true, chefInfo: action.payload }
case CHEF_UPDATE_PROFILE_FAILURE:
return { loading: false, error: action.payload }
case CHEF_UPDATE_PROFILE_RESET:
return { }
default:
return state
}
}
Controller
// #description Get chef profile
// #route GET /api/chefs/profile
// #access Private
const getChefProfile = asyncHandler(async (req, res) => {
const chef = await Chef.findById(req.chef._id)
if(chef) {
res.json({
_id: chef._id,
first_name: chef.first_name,
last_name: chef.last_name,
username: chef.username,
email: chef.email,
bio: chef.bio,
isVegan: chef.isVegan
})
} else {
res.status(404)
throw new Error('Chef not found')
}
})
// #description Update chef profile
// #route PUT /api/chefs/profile
// #access Private
const updateChefProfile = asyncHandler(async (req, res) => {
const chef = await Chef.findById(req.chef._id)
if(chef) {
chef.first_name = req.body.first_name || chef.first_name
chef.last_name = req.body.last_name || chef.last_name
chef.username = req.body.username || chef.username
chef.email = req.body.email || chef.email
chef.bio = req.body.bio || chef.bio
chef.isVegan = req.body.isVegan || chef.isVegan
if (req.body.password) {
chef.password = req.body.password
}
const updatedChef = await chef.save()
res.json({
_id: updatedChef._id,
first_name: updatedChef.first_name,
last_name: updatedChef.last_name,
username: updatedChef.username,
email: updatedChef.email,
bio: updatedChef.bio,
isVegan: updatedChef.isVegan,
token: generateToken(updatedChef._id),
})
} else {
res.status(404)
throw new Error('Chef not found')
}
})
Issue
After much back and forth I believe the issue is with how the response "payload" is stored back in state by the reducer. The response object is a flat object with isVegan at the root, but in state isVegan is in a nested diets array.
res.json({
_id: updatedChef._id,
first_name: updatedChef.first_name,
last_name: updatedChef.last_name,
username: updatedChef.username,
email: updatedChef.email,
bio: updatedChef.bio,
isVegan: updatedChef.isVegan,
token: generateToken(updatedChef._id),
})
The reducer takes the payload and also saves it directly to a chefInfo property and overwriting any existing data.
export const chefUpdateProfileReducer = (state = { }, action) => {
switch(action.type) {
...
case CHEF_UPDATE_PROFILE_SUCCESS:
return { loading: false, success: true, chefInfo: action.payload }
...
}
}
Solution
Reducer should merge in response payload. In your redux screenshot I don't see a chefInfo key so I'll write this to match the screenshot as closely as possible.
export const chefUpdateProfileReducer = (state = { }, action) => {
switch(action.type) {
...
case CHEF_UPDATE_PROFILE_SUCCESS:
const {
_id,
isVegan,
token,
...chefDetails // i.e. first & last name, username, email, bio
} = action.payload;
return {
...state, // <-- shallow copy state
loading: false,
success: true,
chef: {
...state.chef, // <-- shallow copy existing chef details
...chefDetails, // shallow copy new chef details
diets: state.chef.diets.map(diet => diet._id === _id ? { // <-- map existing state
...diet, // <-- shallow copy diet object
isVegan // <-- overwrite isVegan property
} : diet),
},
};
...
}
}
Note: This is a best guess to state structures and types since your reducers appear to have a very minimally defined initial state, so this likely needs to be tweaked to fits your exact state structure.
Hey guy's I have no idea why my state isn't updating in the DOM, I'm sure I'm missing some key react principal. Heres the photo of what my DOM looks like after I submit a post
Instead of the post displaying when I click submit with filled out info, a blank post is shown. And I have to manually reload the page before it shows what was added. I think I'm actually battling some sync issues, please let me know what I can do if you see anything.
I will put the most relevant file's code below and also attache the repository at the bottom if you want to have a complete look.
dataActions.js
import { SET_POSTS, LOADING_DATA, DELETE_POST, POST_PRODUCT, SET_ERRORS, CLEAR_ERRORS, LOADING_UI } from "../types";
import axios from 'axios';
//GET ALL PRODUCTS
export const getPosts = () => dispatch => {
dispatch({ type: LOADING_DATA });
axios.get('/posts')
.then(res => {
dispatch({
type: SET_POSTS,
payload: res.data
})
})
.catch(err => {
dispatch({
type: SET_POSTS,
payload: []
})
})
}
//POST PRODUCT
export const postProduct = (newPost) => (dispatch) => {
dispatch({ type: LOADING_UI });
axios.post('/post', newPost)
.then(res => {
dispatch({
type: POST_PRODUCT,
payload: res.data
})
console.log("success");
dispatch({ type: CLEAR_ERRORS })
})
.catch(err => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
})
})
}
//DELETE PRODUCT
export const deletePost = (postId) => (dispatch) => {
axios.delete(`/post/${postId}`)
.then(() => {
dispatch({ type: DELETE_POST, payload: postId })
})
.catch(err => console.log(err))
}
dataReducer.js
import { SET_POSTS } from '../types';
import { LOADING_DATA, DELETE_POST, POST_PRODUCT/*, SET_POST*/ } from '../types';
const initialState = {
posts: [],
post: {},
loading: false
};
export default function(state = initialState, action){
switch(action.type){
case LOADING_DATA:
return {
...state,
loading: true
}
case SET_POSTS:
return{
...state,
posts: action.payload,
loading: false
}
case DELETE_POST:
let index = state.posts.findIndex(post => post.postId === action.payload);
state.posts.splice(index, 1);
return {
...state
}
case POST_PRODUCT:
return {
...state,
posts: [action.payload, ...state.posts]
}
default:
return state
}
}
PostProduct.js
import React, { Component, Fragment } from "react";
import { withStyles } from "#material-ui/core/styles";
import PropTypes from "prop-types";
import MyButton from "../util/MyButton";
//MUI Stuff
import Button from "#material-ui/core/Button";
import TextField from "#material-ui/core/TextField";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogContent from "#material-ui/core/DialogContent";
import DeleteOutline from "#material-ui/icons/DeleteOutline";
import CircularProgress from '#material-ui/core/CircularProgress';
import AddIcon from '#material-ui/icons/Add';
import CloseIcon from "#material-ui/icons/Close";
//REDUX
import { connect } from "react-redux";
import { postProduct } from "../redux/actions/dataActions";
const styles = {
form: {
textAlign: "center"
},
image: {
margin: "20px auto 20px auto",
width: "50px"
},
pageTitle: {
margin: "10px auto 10px auto"
},
textField: {
margin: "10px auto 10px auto"
},
button: {
marginTop: 20,
postition: "relative"
},
customError: {
color: "red",
fontSixe: "0.8rem",
marginTop: 10
},
progress: {
position: "absolute"
},
submitButton: {
position: "relative"
},
progressSpinner: {
position: 'absolute'
},
closeButton: {
position: 'absolute',
left: '90%',
top: '10%'
}
};
class PostProduct extends Component {
state = {
open: false,
name: '',
errors: {}
};
UNSAFE_componentWillReceiveProps(nextProps){
if (nextProps.UI.errors) {
this.setState({
errors: nextProps.UI.errors
})
}
}
handleOpen = () => {
this.setState({ open: true })
}
handleClose = () => {
this.setState({ open: false })
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value })
}
handleSubmit = (event) => {
event.preventDefault();
this.props.postProduct({ body: this.state.body })
}
render(){
const { errors } = this.state;
const { classes, UI: {loading }} = this.props;
return (
<Fragment>
<MyButton onClick={this.handleOpen} tip="Post a Product">
<AddIcon />
</MyButton>
<Dialog
open={this.state.open}
onClose={this.handleClose}
fullWidth
maxWidth="sm"
>
<MyButton
tip="close"
onClick={this.handleClose}
tipClassName={classes.closeButton}
>
<CloseIcon />
</MyButton>
<DialogTitle>Post the new Product</DialogTitle>
<DialogContent>
<form onSubmit={this.handleSubmit}>
<TextField
name="name"
type="text"
lable="Post Product"
multiline
rows="3"
placeholder="name"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="images"
type="text"
lable="image"
multiline
rows="3"
placeholder="image"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="itemCategory"
type="text"
lable="Painting"
multiline
rows="3"
placeholder="Painting"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="link"
type="text"
lable="link"
multiline
rows="3"
placeholder="https://etsy.com"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="info"
type="text"
lable="blah blah blah"
multiline
rows="3"
placeholder="info"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="price"
type="text"
lable="Price"
multiline
rows="3"
placeholder="75.99"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="available"
type="text"
lable="available?"
multiline
rows="3"
placeholder="true"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="highEnd"
type="text"
lable="High-end or not?"
multiline
rows="3"
placeholder="false"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<Button
type="submit"
variant="contained"
color="primary"
className={classes.submitButton}
disabled={loading}
>
Submit
{loading && (
<CircularProgress
size={30}
className={classes.progressSpinner}
/>
)}
</Button>
</form>
</DialogContent>
</Dialog>
</Fragment>
);
}
} // END CLASS
PostProduct.propTypes = {
postProduct: PropTypes.func.isRequired,
UI: PropTypes.object.isRequired
}
const mapStateToProps = (state) => ({
UI: state.UI
})
export default connect(mapStateToProps, { postProduct })(withStyles(styles)(PostProduct))
The front end code is in this repository here: https://github.com/jIrwinCline/planum-front
Thanks for any help. I know this is a big question...
Post product thunk sets LOADING_UI and then POST_PRODUCT if successful.
export const postProduct = (newPost) => (dispatch) => {
dispatch({ type: LOADING_UI });
axios.post('/post', newPost)
.then(res => {
dispatch({
type: POST_PRODUCT,
payload: res.data
})
console.log("success");
dispatch({ type: CLEAR_ERRORS })
})
.catch(err => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
})
})
In your reducer, there is no LOADING_UI case and thePOST_PRODUCT case just sets the post data but doesn't turn loading off:
case POST_PRODUCT:
return {
...state,
posts: [action.payload, ...state.posts]
}
I suspect you have to add a LOADING_UI case to your reducer and ensure that POST_PRODUCT sets loading to false when it updates your store with the new posts.
Upon submitting the form, I am url encoding the values to then send them in an API request. But when I encode the values, they show up on my form encoded:
Why is it changing the formik values? I'm trying to clone the object which I am encoding in order to not update the formik values but they still update.
Code:
export const uploadAddProductEpic: Epic<*, *, *> = (
action$: ActionsObservable<*>
) =>
action$.ofType(UPLOAD_ADD_PRODUCT).mergeMap((action) => {
const newAction = Object.assign({}, encodeAddProductAction(action))
return ajax
.post('http://192.168.1.12:4000/products', newAction.payload, {
'Content-': 'application/json'
})
.map((response) => uploadAddProductFulfilled(response))
.catch((error) => Observable.of(uploadAddProductRejected(error)))
})
export const handleSubmit = (values, props: AddViewProps): void => {
Toast.show('Uploading Product', {
duration: 3000,
position: 30,
shadow: true,
animation: true,
hideOnPress: true,
delay: 0
})
props.uploadAddProduct(values)
}
export const encodeAddProductAction = (action: VepoAction<Product>) => {
const action1 = Object.assign({}, action)
action1.payload.Shop = encodeURIComponent(
JSON.stringify(action1.payload.Shop)
)
action1.payload.Brand = encodeURIComponent(
JSON.stringify(action1.payload.Brand)
)
action1.payload.Name = encodeURIComponent(
JSON.stringify(action1.payload.Name)
)
action1.payload.Description = encodeURIComponent(
JSON.stringify(action1.payload.Description)
)
return action1
}
const mapStateToProps = (state: RecordOf<VepoState>) => ({
locationListDisplayed: state.formControls.root.locationListDisplayed
})
// eslint-disable-next-line flowtype/no-weak-types
const mapDispatchToProps = (dispatch: Dispatch<*>): Object => ({
uploadAddProduct: (product: Product): void => {
dispatch(uploadAddProduct(product))
}
})
class AddGroceryItemView extends React.Component {
render() {
const {
values,
handleSubmit,
setFieldValue,
errors,
touched,
setFieldTouched,
isValid,
isSubmitting
} = this.props
return (
<Container>
<VepoHeader title={'Add Vegan Grocery Product'} />
<Container style={container}>
<ScrollView
keyboardShouldPersistTaps="always"
style={viewStyle(this.props.locationListDisplayed).scrollView}>
<LocationAutocomplete
label={'Grocery Store'}
placeholder={'Enter Grocery Store'}
setFieldTouched={setFieldTouched}
setFieldValue={setFieldValue}
name="GroceryStore"
required
error={errors.GroceryStore}
touched={touched.GroceryStore}
/>
<View style={viewStyle().detailsContainer}>
<Input
label={'Product Name'}
onTouch={setFieldTouched}
value={values.Name}
placeholder="Enter Name"
name="Name"
required
error={touched.Name && errors.Name}
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<Input
label={'Product Brand'}
value={values.Brand}
onTouch={setFieldTouched}
error={touched.Brand && errors.Brand}
placeholder="Enter Brand"
name="Brand"
required
onChange={setFieldValue}
deleteText={setFieldValue}
/>
<View>
<Input
label={'Product Description'}
value={values.Description}
placeholder="Enter Description"
multiline={true}
required
onTouch={setFieldTouched}
error={touched.Description && errors.Description}
numberOfLines={4}
name="Description"
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<Input
isValid={true}
isPrice={true}
label={'Product Price'}
value={values.Price}
onTouch={setFieldTouched}
error={touched.Price && errors.Price}
placeholder="Enter Price"
name="Price"
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<CategoriesMultiselect.View
error={errors.Categories}
setFieldValue={setFieldValue}
setFieldTouched={setFieldTouched}
touched={touched.Categories}
name="Categories"
required
label="Product Categories"
categoryCodes={[CategoryEnums.CategoryCodes.Grocery]}
/>
<ImagePicker
label="Product Image"
setFieldValue={setFieldValue}
name="Image"
/>
</View>
</View>
</ScrollView>
</Container>
<Button.View
title="submit"
onPress={handleSubmit}
label={'GO!'}
disabled={!isValid || isSubmitting}
loading={isSubmitting}
/>
</Container>
)
}
}
const container = {
flex: 1,
...Spacing.horiz_pad_md_2,
backgroundColor: Colors.grey_lite,
flexDirection: 'column'
}
const formikEnhancer = withFormik({
validationSchema: Yup.object().shape({
Name: Yup.string().required(),
Brand: Yup.string().required(),
GroceryStore: Yup.object()
.shape({
city: Yup.string(),
latitude: Yup.number(),
longitude: Yup.number(),
name: Yup.string(),
place_id: Yup.string(),
street: Yup.string(),
street_number: Yup.string(),
suburb: Yup.string()
})
.required(),
Image: Yup.object().shape({
uri: Yup.string(),
name: Yup.string(),
type: Yup.string()
}),
Categories: Yup.array()
.min(1, 'Please select at least 1 Category')
.required(),
Description: Yup.string()
.min(9)
.required(),
Price: Yup.string().matches(
/^\d+(?:\.\d{2})$/,
'Price must contain 2 decimal places (cents) e.g. 4.00'
)
}),
isInitialValid: false,
mapPropsToValues: () => ({
Name: '',
Brand: '',
Description: '',
Price: '',
Categories: [],
GroceryStore: {},
Image: {}
}),
handleSubmit: (values, { props }) => {
handleSubmit(values, props)
},
displayName: 'AddGroceryItemView'
})(AddGroceryItemView)