From 67732aa00a27aecf1d4cea8cd511276a1e84d040 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 9 Mar 2020 11:32:21 +0100 Subject: [PATCH] UserForm: ajout retour de validation d'API --- frontend/src/actions/user.actions.js | 15 ++++++++ frontend/src/components/UserForm.js | 1 - frontend/src/components/WithForm.js | 10 ++++-- frontend/src/errors/api.error.js | 21 +++++++++++ frontend/src/pages/home.js | 39 ++++++++++++++++---- frontend/src/reducers/users.reducers.js | 24 +++++++++++++ frontend/src/sagas/root.js | 2 ++ frontend/src/sagas/user/root.saga.js | 9 +++++ frontend/src/sagas/user/user.saga.js | 17 +++++++++ frontend/src/services/api-client.service.js | 40 ++++++++++++++++----- frontend/src/store/store.js | 4 ++- 11 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 frontend/src/actions/user.actions.js create mode 100644 frontend/src/errors/api.error.js create mode 100644 frontend/src/reducers/users.reducers.js create mode 100644 frontend/src/sagas/user/root.saga.js create mode 100644 frontend/src/sagas/user/user.saga.js diff --git a/frontend/src/actions/user.actions.js b/frontend/src/actions/user.actions.js new file mode 100644 index 0000000..4904fad --- /dev/null +++ b/frontend/src/actions/user.actions.js @@ -0,0 +1,15 @@ +export const CREATE_USER_REQUEST = 'CREATE_USER_REQUEST'; +export const CREATE_USER_SUCCESS = 'CREATE_USER_SUCCESS'; +export const CREATE_USER_FAILURE = 'CREATE_USER_FAILURE'; + +export function createUser(username, password) { + return { type: CREATE_USER_REQUEST, username, password} +} + +export function createUserSuccess(user) { + return { type: CREATE_USER_SUCCESS, user } +} + +export function createUserFailure(error) { + return { type: CREATE_USER_FAILURE, error } +} \ No newline at end of file diff --git a/frontend/src/components/UserForm.js b/frontend/src/components/UserForm.js index d5391f4..5ce8453 100644 --- a/frontend/src/components/UserForm.js +++ b/frontend/src/components/UserForm.js @@ -4,7 +4,6 @@ import { WithForm } from './WithForm'; export function UserForm({ form, onSubmit }) { useEffect(() => { - console.log(form.valid, form.submitted); if (form.valid && form.submitted) onSubmit(form.data); }, [form.valid, form.submitted]); diff --git a/frontend/src/components/WithForm.js b/frontend/src/components/WithForm.js index 351a039..75910e8 100644 --- a/frontend/src/components/WithForm.js +++ b/frontend/src/components/WithForm.js @@ -4,10 +4,10 @@ export function WithForm(validators) { return function(Component) { - return function WithFormWrapper(props) { - + return function WithFormWrapper({ errors, ...props }) { + const [ formData, setFormData ] = useState({}); - const [ formErrors, setFormErrors ] = useState({}); + const [ formErrors, setFormErrors ] = useState(errors || {}); const [ submitted, setSubmitted ] = useState(false); const validateField = (name) => { @@ -27,6 +27,10 @@ export function WithForm(validators) { setSubmitted(false); }, [submitted]); + useEffect(() => { + setFormErrors(errors || {}); + }, [errors]); + const form = { field: (name, defaultValue) => { return name in formData ? formData[name] : defaultValue; diff --git a/frontend/src/errors/api.error.js b/frontend/src/errors/api.error.js new file mode 100644 index 0000000..1caa9aa --- /dev/null +++ b/frontend/src/errors/api.error.js @@ -0,0 +1,21 @@ +export class APIError extends Error { + constructor(endpoint, code, message, data) { + super(`APIError: ${message}`); + this.endpoint = endpoint; + this.code = code; + this.data = data; + Error.captureStackTrace(this, APIError); + } + + getEndpoint() { + return this.endpoint; + } + + getCode() { + return this.code; + } + + getData() { + return this.data; + } +} diff --git a/frontend/src/pages/home.js b/frontend/src/pages/home.js index a0512e8..d47ff81 100644 --- a/frontend/src/pages/home.js +++ b/frontend/src/pages/home.js @@ -1,23 +1,50 @@ import React from 'react' import Page from './page'; import { ExtendedUserForm as UserForm } from '../components/UserForm'; +import { connect } from 'react-redux'; +import { createUser } from '../actions/user.actions'; -export default class HomePage extends React.PureComponent { - +export class HomePage extends React.PureComponent { render() { - const onUserFormSubmit = formData => { - console.log(formData); - }; + + const { createUserFormError } = this.props; + + const onUserFormSubmit = ({ username, password }) => { + this.props.dispatch(createUser(username, password)); + }; + return (

Bienvenue sur PleaseWait !

Le gestionnaire de ticket simplifié.

Se déconnecter - +
); } } + +function transformErrorCode(code) { + switch (code) { + case 0: + return { + username: { + hasError: true, + message: "Le nom d'utilisateur ne peut pas être vide. (API)" + } + } + default: + return null; + } +} + +function mapStateToProps({ users }) { + return { + createUserFormError: transformErrorCode(users.createUserForm.errorCode), + } +} + +export const ConnectedHomePage = connect(mapStateToProps)(HomePage) diff --git a/frontend/src/reducers/users.reducers.js b/frontend/src/reducers/users.reducers.js new file mode 100644 index 0000000..d6dfdd1 --- /dev/null +++ b/frontend/src/reducers/users.reducers.js @@ -0,0 +1,24 @@ +import { CREATE_USER_FAILURE } from "../actions/user.actions"; + +const initialState = { + byId: {}, + createUserForm: { errorCode: null }, +}; + +export function usersReducer(state = initialState, action) { + switch(action.type) { + case CREATE_USER_FAILURE: + return handleCreateUserFailure(state, action); + }; + return state; +} + +function handleCreateUserFailure(state, { error }) { + return { + ...state, + createUserForm: { + ...state.createUserForm, + errorCode: error.code, + } + }; +} \ No newline at end of file diff --git a/frontend/src/sagas/root.js b/frontend/src/sagas/root.js index f2216bd..09aea3d 100644 --- a/frontend/src/sagas/root.js +++ b/frontend/src/sagas/root.js @@ -2,11 +2,13 @@ import { all } from 'redux-saga/effects'; import { rootSaga as authRootSaga } from './auth/root.saga'; import { rootSaga as failureRootSaga } from './failure/root.saga'; import { rootSaga as initRootSaga } from './init/root.saga'; +import { rootSaga as userRootSaga } from './user/root.saga'; export default function* rootSaga() { yield all([ initRootSaga(), authRootSaga(), failureRootSaga(), + userRootSaga() ]); } diff --git a/frontend/src/sagas/user/root.saga.js b/frontend/src/sagas/user/root.saga.js new file mode 100644 index 0000000..96c98e0 --- /dev/null +++ b/frontend/src/sagas/user/root.saga.js @@ -0,0 +1,9 @@ +import { all, takeLatest } from 'redux-saga/effects'; +import { CREATE_USER_REQUEST } from '../../actions/user.actions'; +import { createUserSaga } from './user.saga'; + +export function* rootSaga() { + yield all([ + takeLatest(CREATE_USER_REQUEST, createUserSaga), + ]); +} diff --git a/frontend/src/sagas/user/user.saga.js b/frontend/src/sagas/user/user.saga.js new file mode 100644 index 0000000..c8ed306 --- /dev/null +++ b/frontend/src/sagas/user/user.saga.js @@ -0,0 +1,17 @@ +import { call, put } from 'redux-saga/effects'; +import { APIClient } from '../../services/api-client.service'; +import { createUserFailure, createUserSuccess } from '../../actions/user.actions'; + +export function* createUserSaga({username, password}) { + const client = new APIClient(); + let user; + try { + user = yield call(client.createUser, username, password); + } catch(err) { + console.error(err); + yield put(createUserFailure(err)); + return + } + + yield put(createUserSuccess(user)); +} \ No newline at end of file diff --git a/frontend/src/services/api-client.service.js b/frontend/src/services/api-client.service.js index 58d2da8..03ffa7d 100644 --- a/frontend/src/services/api-client.service.js +++ b/frontend/src/services/api-client.service.js @@ -1,4 +1,5 @@ import { UnauthorizedError } from '../errors/unauthorized.error.js'; +import { APIError } from '../errors/api.error.js'; export const UnauthorizedStatusCode = 401; @@ -10,10 +11,12 @@ export class APIClient { this.retrieveSessionUser = this.retrieveSessionUser.bind(this); this.listUsers = this.listUsers.bind(this); this.listRequests = this.listRequests.bind(this); + this.createUser = this.createUser.bind(this); } login(username, password) { - return this._callAPI('/login', { username, password }, 'POST') + return this._callAPI('/login', { username, password }, 'POST') + .then(result => result.data); } logout() { @@ -21,7 +24,8 @@ export class APIClient { } retrieveSessionUser() { - return this._callAPI('/me') + return this._callAPI('/me') + .then(result => result.data); } listUsers() { @@ -36,6 +40,12 @@ export class APIClient { } + createUser(username, password) { + return this._callAPI('/users', { username, password }, 'POST') + .then(this._withAPIErrorMiddleware('create_user')) + .then(result => result.data); + } + updateRequestStatus(reqID, newStatus) { } @@ -50,13 +60,27 @@ export class APIClient { credentials: 'include', body: JSON.stringify(body), }) - .then(res => { - if (res.status === UnauthorizedStatusCode) { - throw new UnauthorizedError(); - } - return res; - }) + .then(this._withUnauthorizedErrorMiddleware()) .then(res => res.json()) } + _withUnauthorizedErrorMiddleware() { + return res => { + if (res.status === UnauthorizedStatusCode) { + throw new UnauthorizedError(); + } + return res; + } + } + + _withAPIErrorMiddleware(endpoint) { + return result => { + if (result.error) { + const { code, message, data } = result.error; + throw new APIError(endpoint, code, message, data); + } + return result; + } + } + } diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 59aeb1d..39ba660 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -4,13 +4,15 @@ import rootSaga from '../sagas/root' import { sessionReducer } from '../reducers/session.reducers'; import { messagesReducer } from '../reducers/messages.reducers'; import project from '../reducers/project' +import { usersReducer } from '../reducers/users.reducers'; const sagaMiddleware = createSagaMiddleware() const rootReducer = combineReducers({ session: sessionReducer, messages: messagesReducer, - project + project, + users: usersReducer, }); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;