Commit e910362f authored by Remy Huet's avatar Remy Huet

Merge branch 'feature/categories' into develop

parents 857ceae5 fd21d29e
Pipeline #31975 passed with stage
in 58 seconds
......@@ -4,5 +4,8 @@
"env": {
"browser": true,
"jest": true
},
"rules": {
"no-cond-assign":"off"
}
}
\ No newline at end of file
......@@ -3909,6 +3909,11 @@
"is-symbol": "^1.0.2"
}
},
"es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
......@@ -7898,6 +7903,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"lodash-es": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz",
"integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
......@@ -11678,6 +11688,28 @@
"symbol-observable": "^1.2.0"
}
},
"redux-form": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.4.2.tgz",
"integrity": "sha512-QxC36s4Lelx5Cr8dbpxqvl23dwYOydeAX8c6YPmgkz/Dhj053C16S2qoyZN6LO6HJ2oUF00rKsAyE94GwOUhFA==",
"requires": {
"es6-error": "^4.1.1",
"hoist-non-react-statics": "^2.5.4",
"invariant": "^2.2.4",
"is-promise": "^2.1.0",
"lodash": "^4.17.10",
"lodash-es": "^4.17.10",
"prop-types": "^15.6.1",
"react-lifecycles-compat": "^3.0.4"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
}
}
},
"redux-logger": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
......
......@@ -17,7 +17,7 @@ function App(props) {
<PageContainer>
<>
<Route path="/login" component={Login} />
<PrivateRoute path="/categories" component={Categories} userIdentified={userIdentified} />
<PrivateRoute path="/categories/:action?/:category?" component={Categories} userIdentified={userIdentified} />
<PrivateRoute path="/protected" component={Protected} userIdentified={userIdentified} />
</>
</PageContainer>
......
......@@ -4,13 +4,41 @@ import env from '../env';
/* eslint-enable import/no-unresolved */
export function fetchCategories(token) {
axios.defaults.headers.common = {
Authorization: `Bearer ${token}`,
};
return {
type: 'FETCH_CATEGORIES',
payload: axios.get(`${env.api_uri}/api/v1/categories`),
payload: axios.get(`${env.api_uri}/api/v1/categories`,
{
headers: { Authorization: `Bearer ${token}` },
}),
};
}
export function deleteCategory(token, id) {
return {
type: 'DELETE_CATEGORY',
payload: axios.delete(`${env.api_uri}/api/v1/categories/${id}`,
{
headers: { Authorization: `Bearer ${token}` },
}),
};
}
export function createCategory(token, data) {
return {
type: 'CREATE_CATEGORY',
payload: axios.post(`${env.api_uri}/api/v1/categories`, { parent_id: data.parentId, ...data },
{
headers: { Authorization: `Bearer ${token}` },
}),
};
}
export function a() {}
export function updateCategory(token, id, data) {
return {
type: 'UPDATE_CATEGORY',
payload: axios.put(`${env.api_uri}/api/v1/categories/${id}`, { parent_id: data.parentId, ...data },
{
headers: { Authorization: `Bearer ${token}` },
}),
};
}
import React from 'react';
import PropTypes from 'prop-types';
import { Card, Button } from 'semantic-ui-react';
export default function Category({ name }) {
return (
<Card raised>
<Card.Content>
<Card.Header>
{name}
</Card.Header>
</Card.Content>
<Card.Content extra>
<Button.Group>
<Button primary>Voir</Button>
<Button.Or text="ou" />
<Button negative>Supprimer</Button>
</Button.Group>
</Card.Content>
</Card>
);
}
Category.propTypes = {
name: PropTypes.string.isRequired,
};
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { submit } from 'redux-form';
import { Link } from 'react-router-dom';
import { Confirm, Header, Button } from 'semantic-ui-react';
import CategoryForm from '../../forms/Category';
import { createCategory } from '../../actions/categoriesActions';
function CategoryCreate({ dispatch, userToken, callback }) {
const handleFormSubmit = (values) => {
dispatch(createCategory(userToken, values)).then(callback).catch(callback);
};
return (
<Confirm
open
cancelButton={<Button as={Link} to="/categories">Retour</Button>}
confirmButton={<Button primary onClick={() => dispatch(submit('category'))}> OK </Button>}
header={<Header as="h1">Créer une catégorie</Header>}
content={<CategoryForm onSubmit={handleFormSubmit} />}
/>
);
}
CategoryCreate.propTypes = {
dispatch: PropTypes.func.isRequired,
userToken: PropTypes.string.isRequired,
callback: PropTypes.func.isRequired,
};
export default connect(store => ({
userToken: store.user.token,
}))(CategoryCreate);
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Confirm, Button, Header } from 'semantic-ui-react';
import { deleteCategory } from '../../actions/categoriesActions';
function CategoryDelete({
name, id, userToken, dispatch, callback,
}) {
return (
<Confirm
open
cancelButton={<Button as={Link} to="/categories">Retour</Button>}
confirmButton={(
<Button
onClick={() => dispatch(deleteCategory(userToken, id)).then(callback).catch(callback)}
content="Oui"
/>
)}
header={<Header as="h1">Supprimer</Header>}
content={(
<>
<Header as="h3">{`Êtes-vous sûr de vouloir supprimer la catégorie ${name} ?`}</Header>
<p>La catégorie doit être vide</p>
</>
)}
/>
);
}
CategoryDelete.propTypes = {
name: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
userToken: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired,
callback: PropTypes.func.isRequired,
};
export default connect(store => ({
userToken: store.user.token,
}))(CategoryDelete);
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Confirm, Button, Header } from 'semantic-ui-react';
import { submit } from 'redux-form';
import CategoryForm from '../../forms/Category';
import { updateCategory } from '../../actions/categoriesActions';
function CategoryEdit({
id, name, parentId, dispatch, callback, userToken,
}) {
const handleFormSubmit = (values) => {
dispatch(updateCategory(userToken, id, values)).then(callback).catch(callback);
};
return (
<Confirm
open
cancelButton={<Button as={Link} to={`/categories/show/${id}`}>Retour</Button>}
confirmButton={<Button primary onClick={() => (dispatch(submit('category')))}> OK </Button>}
header={<Header as="h1">Éditer</Header>}
content={
<CategoryForm onSubmit={handleFormSubmit} defaultValues={{ name, parentId }} />
}
/>
);
}
CategoryEdit.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
parentId: PropTypes.number.isRequired,
dispatch: PropTypes.func.isRequired,
callback: PropTypes.func.isRequired,
userToken: PropTypes.string.isRequired,
};
export default connect(store => ({
userToken: store.user.token,
}))(CategoryEdit);
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Card, Button } from 'semantic-ui-react';
function CategoryIndex({
name, userType, parentId, parentName, id,
}) {
return (
<Card raised>
<Card.Content>
<Card.Header>
{name}
</Card.Header>
{parentName && (
<Card.Meta as={Link} to={`/categories/show/${parentId}`}>
{'Sous catégorie de '}
<u>{parentName}</u>
</Card.Meta>
)}
</Card.Content>
<Card.Content extra>
<Button.Group>
<Button primary as={Link} to={`/categories/show/${id}`}>Voir</Button>
{userType === 'Responsable' && (
<>
<Button.Or text="ou" />
<Button negative as={Link} to={`/categories/delete/${id}`}>Supprimer</Button>
</>)}
</Button.Group>
</Card.Content>
</Card>
);
}
CategoryIndex.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
userType: PropTypes.string.isRequired,
parentId: PropTypes.number,
parentName: PropTypes.string,
};
CategoryIndex.defaultProps = {
parentId: null,
parentName: null,
};
export default connect(store => ({
userType: store.user.user.type,
}))(CategoryIndex);
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import {
Confirm, Button, Header, Breadcrumb,
} from 'semantic-ui-react';
export default function CategoryShow({
name, parentId, parentName, id,
}) {
return (
<Confirm
open
cancelButton={<Button as={Link} to={`/categories/edit/${id}`} color="orange">Éditer</Button>}
confirmButton={<Button primary as={Link} to="/categories">Fermer</Button>}
header={<Header as="h1">{name}</Header>}
content={
<>
{parentId && (
<Breadcrumb>
<Breadcrumb.Section link as={Link} to={`/categories/show/${parentId}`}>{parentName}</Breadcrumb.Section>
<Breadcrumb.Divider icon="right angle" />
<Breadcrumb.Section active>{name}</Breadcrumb.Section>
</Breadcrumb>
)}
</>
}
/>
);
}
CategoryShow.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
parentId: PropTypes.number,
parentName: PropTypes.string,
};
CategoryShow.defaultProps = {
parentId: null,
parentName: null,
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import {
Form, Input, Container, Segment,
} from 'semantic-ui-react';
import Dropdown from './CustomDropdown';
class CategoryForm extends Component {
componentDidMount() {
const { initialize, defaultValues } = this.props;
initialize(defaultValues);
}
render() {
const { categories, handleSubmit, defaultValues } = this.props;
const parentOptions = categories.map(({ name, id }) => ({
key: id,
value: id,
text: name,
}));
return (
<Container>
<Segment>
<Form onSubmit={handleSubmit}>
<Form.Field>
<Field name="name" component={Input} placeholder="Nom" />
</Form.Field>
<Form.Field>
<Field name="parentId" defaultValue={defaultValues.parentId} component={Dropdown} search selection clearable options={parentOptions} />
</Form.Field>
</Form>
</Segment>
</Container>
);
}
}
CategoryForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
categories: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
})).isRequired,
defaultValues: PropTypes.shape({
name: PropTypes.string,
parentId: PropTypes.number,
}),
initialize: PropTypes.func.isRequired,
};
CategoryForm.defaultProps = {
defaultValues: {
name: null,
parentId: null,
},
};
const form = reduxForm({
form: 'category',
})(CategoryForm);
export default connect(store => ({
categories: store.categories.categories,
}))(form);
import React from 'react';
import { Dropdown } from 'semantic-ui-react';
export default function CustomDropdown({ currentValue, input, ...rest }) {
return (
<Dropdown
onChange={(event, data) => input.onChange(data.value)}
value={currentValue}
{...rest}
/>
);
}
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import {
Dimmer, Loader, Container, Card,
Dimmer, Loader, Container, Card, Button, Divider, Message,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { fetchCategories } from '../actions/categoriesActions';
import Category from '../components/Category';
import CategoryIndex from '../components/Category/Index';
import CategoryShow from '../components/Category/Show';
import CategoryEdit from '../components/Category/Edit';
import CategoryDelete from '../components/Category/Delete';
import CategoryCreate from '../components/Category/Create';
function Categories({
categoriesFetched, categoriesFetching, categories, dispatch, userToken,
categoriesFetched,
categoriesFetching,
categories,
dispatch,
userToken,
match,
history,
categoriesAction,
categoriesActionInfo,
categoriesActionSuccess,
}) {
/* eslint-disable camelcase */
const data = categories.map(({ id, name, parent_id }) => (
<Category key={id} name={name} parentId={parent_id} />));
/* eslint-enable camelcase */
const { params } = match;
// Map all categories in array of <Category />
const index = categories.map(({
id, name, parent_id: parentId, parent,
}) => (
<CategoryIndex
key={id}
id={id}
name={name}
parentId={parentId}
parentName={parent ? parent.name : null}
/>));
// Get single category attributes
let cat;
const {
name = null,
id = null,
parent_id: parentId = null,
parent = null,
} = (params.category && (cat = categories.find(c => c.id === parseInt(params.category, 10))))
? cat
: {};
if (!categoriesFetched && !categoriesFetching) {
dispatch(fetchCategories(userToken));
}
return (
<Container style={{ margin: '2em' }}>
<Container style={{ margin: '1em' }}>
{categoriesFetching && (
<Dimmer active>
<Loader>Chargement en cours</Loader>
</Dimmer>)}
{categoriesAction && categoriesActionSuccess !== null && (
<Message
floated="center"
floating
header={categoriesActionInfo}
positive={categoriesActionSuccess}
negative={!categoriesActionSuccess}
onDismiss={() => dispatch({ type: 'CATEGORY_MESSAGE_DISMISS' })}
/>
)}
<Button floated="right" style={{ marginBottom: '1em' }} color="orange" icon="redo" onClick={() => dispatch(fetchCategories(userToken))} />
<Button floated="right" style={{ marginBottom: '1em' }} positive icon="plus" as={Link} to="/categories/create" />
<Divider clearing />
<Card.Group centered>
{data}
{index}
</Card.Group>
{params.action && ({
show: (
<CategoryShow
name={name}
parentId={parentId}
parentName={parent ? parent.name : null}
id={id}
/>
),
edit: (
<CategoryEdit
id={id}
name={name}
parentId={parentId}
callback={() => history.push('/categories')}
/>
),
delete: (
<CategoryDelete
id={id}
name={name}
callback={() => history.push('/categories')}
/>
),
create: (
<CategoryCreate
callback={() => history.push('/categories')}
/>
),
}[params.action])}
</Container>
);
}
......@@ -43,15 +124,34 @@ Categories.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
parent_id: PropTypes.number,
parent: PropTypes.shape({ name: PropTypes.string }),
}),
).isRequired,
dispatch: PropTypes.func.isRequired,
userToken: PropTypes.string.isRequired,
categoriesAction: PropTypes.string,
categoriesActionInfo: PropTypes.string,
categoriesActionSuccess: PropTypes.bool,
match: PropTypes.shape({
params: PropTypes.object,
}).isRequired,
history: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
};
Categories.defaultProps = {
categoriesAction: null,
categoriesActionInfo: null,
categoriesActionSuccess: null,
};
export default connect(store => ({
categoriesFetched: store.categories.fetched,
categoriesFetching: store.categories.fetching,
categories: store.categories.categories,
categoriesAction: store.categories.action,
categoriesActionSuccess: store.categories.success,
categoriesActionInfo: store.categories.info,
userToken: store.user.token,
}))(Categories);
const initialState = {
fetching: false,
fetched: false,
action: null,
success: null,
info: null,
categories: [],
};
......@@ -11,6 +14,9 @@ export default function reducer(state = initialState, action) {
...state,
fetching: true,
fetched: false,
action: null,
success: null,
info: null,
};
}
case ('FETCH_CATEGORIES_FULFILLED'): {
......@@ -21,6 +27,82 @@ export default function reducer(state = initialState, action) {
categories: action.payload.data,
};
}
case ('DELETE_CATEGORY_PENDING'): {
return {
...state,
action: 'DELETE',
success: null,
};
}
case ('DELETE_CATEGORY_FULFILLED'): {
return {
...state,
categories: state.categories.filter(category => category.id !== action.payload.data.id),
success: true,
info: 'Catégorie supprimée avec succès',
};
}
case ('DELETE_CATEGORY_REJECTED'): {
return {
...state,
success: false,
info: 'Impossible de supprimer la catégorie',
};
}
case ('CREATE_CATEGORY_PENDING'): {
return {
...state,
action: 'CREATE',
success: null,
};
}
case ('CREATE_CATEGORY_FULFILLED'): {
return {
...state,
categories: [...state.categories, action.payload.data],
success: true,
info: 'Catégorie créée avec succès',
};
}
case ('CREATE_CATEGORY_REJECTED'): {
return {
...state,
success: false,
info: 'Impossible de créer la catégorie',
};
}
case ('UPDATE_CATEGORY_PENDING'): {
return {
...state,
action: 'UPDATE',
success: null,
};
}
case ('UPDATE_CATEGORY_FULFILLED'): {
return {
...state,
categories: state.categories.map(
category => (category.id === action.payload.data.id ? action.payload.data : category),
),
success: true,
info: 'Catégorie mise à jour avec succès',
};
}
case ('UPDATE_CATEGORY_REJECTED'): {
return {
...state,
success: false,
info: 'Impossible de mettre à jour la catégorie',
};
}
case ('CATEGORY_MESSAGE_DISMISS'): {
return {
...state,
action: null,