Commit a12546bf authored by Daniel Caillibaud's avatar Daniel Caillibaud

WIP version intermédiaire avec un state sans redux pour les membres

parent d2a735ee
......@@ -7,10 +7,11 @@ import {
getGroupeJoinUrl,
getGroupeLeaveUrl,
getGroupesPersoUrl,
getMembresUrl,
getSaveGroupeUrl
} from '../apiRoutes'
const dummyCb = () => {}
export const loadGroupes = () => dispatch => {
const requestSuccess = ({
groupes,
......@@ -32,27 +33,6 @@ export const loadGroupes = () => dispatch => {
.then(requestSuccess, requestError)
}
// la limite par défaut est à 100 dans le contrôleur, on le gère pas coté front
export const loadMembres = ({nom, skip}) => dispatch => {
const requestSuccess = ({
membres,
nbAbonnes,
skip
}) => dispatch({
type: 'LOAD_MEMBRES',
payload: {membres, nbAbonnes}
})
const requestError = (error) => dispatch(
addNotification({
level: 'error',
message: `Le chargement des membres a échoué : ${error.message}`
})
)
return GET(getMembresUrl({nom, skip}))
.then(requestSuccess, requestError)
}
const saveGroupeAction = (groupe, isNew) => ({
type: 'SAVE_GROUPE',
payload: {
......@@ -89,7 +69,7 @@ const deleteGroupeAction = (nom) => ({
payload: { nom }
})
export const deleteGroupe = (nom, success = () => {}) => dispatch => {
export const deleteGroupe = (nom, success = dummyCb) => dispatch => {
const requestSuccess = () => {
dispatch(
addNotification({
......@@ -118,7 +98,7 @@ const leaveGroupeAction = (nom) => ({
export const leaveGroupe = (
nom,
success = () => {}
success = dummyCb
) => dispatch => {
const requestSuccess = () => {
dispatch(
......@@ -148,7 +128,7 @@ const ignoreGroupeAction = (nom) => ({
export const ignoreGroupe = (
nom,
success = () => {}
success = dummyCb
) => dispatch => {
const requestSuccess = () => {
dispatch(
......@@ -179,7 +159,7 @@ const joinGroupeAction = groupe => ({
export const joinGroupe = (
nom,
success = () => {}
success = dummyCb
) => dispatch => {
const requestSuccess = (groupe) => {
dispatch(
......@@ -209,7 +189,7 @@ const followGroupeAction = groupe => ({
export const followGroupe = (
nom,
success = () => {}
success = dummyCb
) => dispatch => {
const requestSuccess = (groupe) => {
dispatch(
......
import {GET, POST} from '../utils/httpMethods'
import {addNotification} from './notifications'
import {
getGroupeMembresUrl,
getGroupeRetirerMembreUrl
} from '../apiRoutes'
// la limite par défaut est à 100 dans le contrôleur, on le gère pas coté front
export const loadMembres = ({nom, skip}) => (dispatch) => GET(getGroupeMembresUrl({nom, skip}))
.then(({membres, nbMembres, nbAbonnes, skip, limit}) => dispatch({
type: 'LOAD_MEMBRES',
payload: {membres, nbMembres, nbAbonnes, skip, limit}
})).catch((error) => dispatch(
addNotification({
level: 'error',
message: `Le chargement des membres a échoué : ${error.message}`
})
))
export const retirerMembre = ({groupeNom, personneOid}) => (dispatch) => {
const url = getGroupeRetirerMembreUrl()
const body = {
groupeNom,
personneOid
}
return POST(url, {body}).then(({membre}) => {
dispatch(
addNotification({
level: 'info',
message: `Membre retiré : ${membre.prenom} ${membre.nom} (${membre.pid})`
})
)
dispatch({
type: 'RETIRER_MEMBRE',
payload: {membre}
})
}).catch(error => {
console.log(error)
dispatch(
addNotification({
level: 'error',
message: error.message
})
)
})
}
......@@ -84,7 +84,7 @@ const membres = ({nom, skip, limit}) => {
return url
}
/**
* Retourne l'url pour récupérer les membres du groupe
* Liste des membres du groupe
* @type {routeGetter}
* @param {object} urlParams
* @param {string} urlParams.nom Le nom du groupe
......@@ -92,7 +92,21 @@ const membres = ({nom, skip, limit}) => {
* @param {number} [urlParams.limit] limit max du nb de personnes qui seront retournées
* @param {string} [baseUrl]
*/
export const getMembresUrl = makeGetter(membres, ['nom'])
export const getGroupeMembresUrl = makeGetter(membres, ['nom'])
const groupeAjouterMembre = () => 'groupe/ajouterMembre'
/**
* Ajouter un membre à un groupe (cf doc du controleur pour les params à passer en post)
* @type {routeGetter}
*/
export const getGroupeAjouterMembreUrl = makeGetter(groupeAjouterMembre)
const groupeRetirerMembre = () => 'groupe/retirerMembre'
/**
* Retirer un membre d'un groupe (cf doc du controleur pour les params à passer en post)
* @type {routeGetter}
*/
export const getGroupeRetirerMembreUrl = makeGetter(groupeRetirerMembre)
const clone = ({oid}) => `clone/${oid}`
/**
......
......@@ -111,7 +111,6 @@ const getInitialValues = ({
match: {params: {groupe: groupeNom}},
// vient de la session (via ensureLogged appelé par groupesLoader)
personne: {
oid,
nom,
prenom
}
......
......@@ -2,6 +2,10 @@ import PropTypes from 'prop-types'
import React, {Fragment} from 'react'
import {NavLink} from 'react-router-dom'
import groupeMembresLoader from './hoc/groupeMembresLoader'
import {saveGroupe} from 'client-react/actions/groupes'
import validate, {asyncBlurFields, asyncValidate} from 'client-react/utils/groupeValidate'
import onSubmitFail from 'client-react/utils/onSubmitFail'
import groupesLoader from 'client-react/components/groupes/hoc/groupesLoader'
const getHeader = ({nbMembres, skip, limit, length}) => {
if (nbMembres === 0) return 'Aucun membre'
......@@ -28,7 +32,9 @@ const GroupeListerMembres = ({
skip,
limit,
previous,
next
next,
ajouterMembre,
retirerMembre
}) => (
<Fragment>
<div className="fr">
......@@ -39,8 +45,27 @@ const GroupeListerMembres = ({
{previous ? (<a onClick={previous}>&lt;&lt;précédents</a>) : null}
<span style={{margin: '0 1rem'}}>{getHeader({nbMembres, skip, limit, length: (membres && membres.length) || 0})}</span>
{next ? (<a onClick={next}>suivants &gt;&gt;</a>) : null}
{membres.map(({nom, prenom}, index) => (<li key={index}>{prenom} {nom}</li>))}
{membres.map(({nom, prenom, oid}, index) => (<li key={index}>{prenom} {nom} <button onClick={retirerMembre({oid})}>Retirer du groupe</button></li>))}
</ul>
<label className="field">Ajouter un membre <i>(en le faisant vous vous engagez à lui avoir demandé son avis avant)</i></label>
<form className="grid-6" onSubmit={(event, values, ...rest) => {
event.preventDefault()
console.log('onSubmit', Object.keys(event), event.target, event)
const {nom, pid} = event.data || {}
ajouterMembre({nom, pid})
}}>
<div className="field col-2">
<label htmlFor="form-membres-pid">Identifiant</label>
<input id="form-membres-pid" type="text" name="pid" placeholder="cf. « mes informations personnelles »"/>
</div>
<div className="field col-3">
<label htmlFor="form-membres-nom">Son nom</label>
<input id="form-membres-nom" name="nom" placeholder="Entrez également le nom (insensible à la casse)" />
</div>
<div style={{position: 'relative'}}>
<button type="submit" className="btn--primary" style={{position: 'absolute', bottom: '12px', left: '1rem'}}>Ajouter</button>
</div>
</form>
</Fragment>
)
......@@ -52,8 +77,10 @@ GroupeListerMembres.propTypes = {
skip: PropTypes.number,
limit: PropTypes.number,
previous: PropTypes.func,
next: PropTypes.func
next: PropTypes.func,
ajouterMembre: PropTypes.func,
retirerMembre: PropTypes.func
}
// groupesListeLoader contient ensureLogged
export default groupeMembresLoader()(GroupeListerMembres)
export default groupeMembresLoader(GroupeListerMembres)
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {GET} from 'client-react/utils/httpMethods'
import {GET, POST} from 'client-react/utils/httpMethods'
import ensureLogged from 'client-react/hoc/ensureLogged'
import addNotify from 'client-react/hoc/addNotify'
import getDisplayName from 'client-react/utils/getDisplayName'
import {getMembresUrl} from 'client-react/apiRoutes'
import {getGroupeAjouterMembreUrl, getGroupeMembresUrl, getGroupeRetirerMembreUrl} from 'client-react/apiRoutes'
/**
* @callback hocWrapper
* @param {Component} ComponentToWrap
* @return {Component} Le composant enrichi
*/
/**
* Higher Order Component qui charge les membres d'un groupe (passera au component wrappé les props membres, nbMembres, nbAbonnes)
* @return {hocWrapper}
* @param {Component} WrappedComponent
* @return {Component} WrappedComponent enrichi
*/
const groupeMembresLoader = () => WrappedComponent => {
const groupeMembresLoader = WrappedComponent => {
class GroupeMembresLoader extends Component {
constructor (props) {
console.log('constructeur GroupeMembresLoader avec les props', props)
......@@ -27,13 +24,70 @@ const groupeMembresLoader = () => WrappedComponent => {
const urlOptions = {nom: this.state.nom}
if (skip) urlOptions.skip = skip
// pour debug suivant / précédent fixer ici une limite basse, genre urlOptions.limit = 3
const url = getMembresUrl(urlOptions)
const url = getGroupeMembresUrl(urlOptions)
GET(url)
.then(({membres, nbMembres, nbAbonnes, skip, limit}) => this.setState({membres, nbMembres, nbAbonnes, skip, limit}))
.catch(error => {
console.log(error)
this.setState({errorMessage: error.message})
if (error.status === 403) {
// On affiche le message d'erreur sur la page, plus clair et évite une page vide
this.setState({errorMessage: error.message})
} else {
console.error(error)
this.props.notify({level: 'error', message: error.message})
}
})
}
ajouterMembre ({nom, pid}) {
const url = getGroupeAjouterMembreUrl()
const body = {
groupeNom: this.state.nom,
personneNom: nom,
pid
}
POST(url, {body}).then(({membre}) => {
this.props.notify(`Membre ajouté ${membre.prenom} ${membre.nom} (${membre.pid})`)
const membres = []
let toAdd = true
this.state.membres.forEach((m) => {
// on l'insère dès que c'est sa place dans l'ordre alpha
if (toAdd && membre.nom < m.nom) {
membres.push(membre)
toAdd = false
}
membres.push(m)
})
// pour éviter de refaire une requête et conserver un affichage cohérent on incrémente la limite
const newStateProps = {
membres,
nbMembres: this.state.nbMembres + 1,
limit: this.state.limit + 1
}
this.setState(newStateProps)
}).catch(error => {
console.log(error)
this.props.notify({level: 'error', message: error.message})
})
}
retirerMembre ({oid}) {
const url = getGroupeRetirerMembreUrl()
const body = {
groupeNom: this.state.nom,
personneOid: oid
}
POST(url, {body}).then(({membre}) => {
this.props.notify(`Membre supprimé : ${membre.prenom} ${membre.nom} (${membre.pid})`)
const newStateProps = {
membres: this.state.membres.filter(m => m.oid !== membre.oid),
nbMembres: this.state.nbMembres - 1,
limit: this.state.limit - 1
}
this.setState(newStateProps)
}).catch(error => {
console.log(error)
this.props.notify({level: 'error', message: error.message})
})
}
componentDidMount () {
......@@ -50,6 +104,8 @@ const groupeMembresLoader = () => WrappedComponent => {
if (this.state.nbMembres > this.state.skip + this.state.limit) {
childProps.next = () => this.refresh(this.state.skip + this.state.limit)
}
childProps.ajouterMembre = this.ajouterMembre.bind(this)
childProps.retirerMembre = this.retirerMembre.bind(this)
return (
<WrappedComponent {...childProps} />
......@@ -64,10 +120,11 @@ const groupeMembresLoader = () => WrappedComponent => {
params: PropTypes.shape({
groupe: PropTypes.string
})
})
}),
notify: PropTypes.func
}
return ensureLogged(GroupeMembresLoader)
return ensureLogged(addNotify(GroupeMembresLoader))
}
export default groupeMembresLoader
import {connect} from 'react-redux'
import {addNotification} from 'client-react/actions/notifications'
function _notify (dispatch, notification) {
if (!notification) return console.error(Error('notify appelé sans rien à notifier'))
if (notification instanceof Error) {
console.error(notification)
return dispatch(addNotification({message: notification.message, level: 'error'}))
}
// on accepte les string
const message = typeof notification === 'string' ? notification : notification.message
if (typeof message !== 'string') return console.error(Error('appel incorrect de notify'), notification)
if (!message) return console.error(Error('notify appelé avec un message vide'))
const level = notification.level || 'info'
dispatch(addNotification({message, level}))
}
const mapDispatchToProps = (dispatch) => ({
/**
* Affiche une notification
* @param {string|object|Error} notification Si c'est une Error, elle sera affichée en console et le message en notification, sinon il faut une string ou un objet avec au moins une propriété message
* @param {string} notification.message
* @param {string} [notification.level=info]
* @return {*}
*/
notify: _notify.bind(null, dispatch)
})
/**
* High Order Component qui fournit une méthode notify aux props de WrappedComponent
* @param {Component} WrappedComponent
* @return {Component} Le composant enrichi
*/
const addNotify = (WrappedComponent) => connect(null, mapDispatchToProps)(WrappedComponent)
export default addNotify
......@@ -71,9 +71,9 @@ module.exports = function (component) {
if (!data.oid && !$accessControl.hasGenericPermission('createGroupe', context)) return $json.denied(context, 'Vous n’avez pas les droits suffisants pour créer un groupe')
flow().seq(function () {
if (data.oid) return EntityGroupe.match('oid').equals(data.oid).grabOne(this)
if (data.oid) return $groupeRepository.load(data.oid)
// à la création on cherche d'après le nom pour vérifier qu'il n'existe pas
EntityGroupe.match('nom').equals(data.nom).grabOne(this)
$groupeRepository.loadByNom(data.nom, this)
}).seq(function (groupeBdd) {
const nextStep = this
if (data.oid) {
......@@ -147,6 +147,65 @@ module.exports = function (component) {
})
controller.options('', optionsOk)
/**
* Ajoute un membre à un groupe. Il faut les propriétés groupeNom, personneNom, pid
* @route POST /api/groupe/ajouterMembre
*/
controller.post('ajouterMembre', function (context) {
const {groupeNom, personneNom, pid} = context.post
const myOid = $accessControl.getCurrentUserOid(context)
// checks de base
if (!myOid) return $json.denied(context, 'Vous devez être authentifié pour ajouter un membre à un groupe')
if (!groupeNom) return $json.sendKo(context, 'Impossible de modifier un groupe sans nom')
if (!personneNom || !pid || typeof personneNom !== 'string' || typeof pid !== 'string') return $json.sendKo(context, 'Paramètres invalides')
// on charge le groupe pour vérifier qu'on en est gestionnaire
let groupe
flow().seq(function () {
$groupeRepository.loadByNom(groupeNom, this)
}).seq(function (_groupe) {
if (!isManaged(context, _groupe)) return $json.denied(context, 'Vous n’êtes pas gestionnaire de ce groupe et ne pouvez pas modifier ses membres')
groupe = _groupe
$personneRepository.loadByPidAndNom(pid, personneNom, this)
}).seq(function (personne) {
$personneRepository.ajouterGroupesMembre(personne, groupe, this)
}).seq(function (personne) {
// on renvoie pas tout
const {nom, oid, pid, prenom} = personne
$json.sendOk(context, {membre: {nom, oid, pid, prenom}})
}).catch((error) => $json.sendKo(context, error))
})
/**
* Retire un membre d'un groupe. Il faut les propriétés groupeNom, personneOid
* @route POST /api/groupe/retirerMembre
*/
controller.post('retirerMembre', function (context) {
const {groupeNom, personneOid} = context.post
const myOid = $accessControl.getCurrentUserOid(context)
// checks de base
if (!myOid) return $json.denied(context, 'Vous devez être authentifié pour ajouter un membre à un groupe')
if (!groupeNom) return $json.sendKo(context, 'Impossible de modifier un groupe sans nom')
if (!groupeNom || !personneOid || typeof groupeNom !== 'string' || typeof personneOid !== 'string') return $json.sendKo(context, 'Paramètres invalides')
// on charge le groupe pour vérifier qu'on en est gestionnaire
let groupe
flow().seq(function () {
$groupeRepository.loadByNom(groupeNom, this)
}).seq(function (_groupe) {
if (!isManaged(context, _groupe)) return $json.denied(context, 'Vous n’êtes pas gestionnaire de ce groupe et ne pouvez pas modifier ses membres')
groupe = _groupe
$personneRepository.load(personneOid, this)
}).seq(function (personne) {
if (!personne) return $json.sendKo(context, `La personne ${personneOid} n’existe pas`)
$personneRepository.retirerGroupesMembre(personne, groupe, this)
}).seq(function (personne) {
// on renvoie pas tout
const {nom, oid, pid, prenom} = personne
$json.sendOk(context, {membre: {nom, oid, pid, prenom}})
}).catch((error) => $json.sendKo(context, error))
})
/**
* Récupère un groupe (d'après son oid)
* @route GET /api/groupe/:oid
......
......@@ -49,7 +49,7 @@ module.exports = function (component) {
* @param {string[]} [options.whiteList] Une liste de noms de groupes que l'on peut ajouter sans vérifier si on est membre ou pas
* @param {errorCallback} next
*/
function addGroupe (context, ressource, groupeNom, options, next) {
function addGroupeToRess (context, ressource, groupeNom, options, next) {
if (!ressource) throw new Error('pas de ressource')
if (!groupeNom) throw new Error('pas de nom de groupe')
if (arguments.length === 4) {
......@@ -171,14 +171,14 @@ module.exports = function (component) {
const options = {
whiteList: (ressourceOriginale && ressourceOriginale.groupes) || []
}
addGroupe(context, ressourceNew, groupeNom, options, this)
addGroupeToRess(context, ressourceNew, groupeNom, options, this)
// @todo faudrait vérifier que l'on a pas viré de groupe auxquel on appartient pas, et si c'était le cas cloner ?
}).set(groupesAuteursVoulus).seqEach(function (groupeNom) {
const options = {
whiteList: (ressourceOriginale && ressourceOriginale.groupesAuteurs) || [],
isGroupeAuteur: true
}
addGroupe(context, ressourceNew, groupeNom, options, this)
addGroupeToRess(context, ressourceNew, groupeNom, options, this)
}).seq(function () {
next(null, ressourceNew)
}).catch(addErrorAndNext)
......
......@@ -40,40 +40,23 @@ const {looksLikePid} = require('../../utils/validators')
module.exports = function (component) {
component.service('$personneRepository', function (EntityPersonne, EntityGroupe, $cachePersonne, $groupeRepository) {
/**
* Ajoute un groupe à la personne, comme membre (en le créant s'il n'existait pas),
* @param {Personne|EntityPersonne} personne (sa prop groupesMembre sera modifiée)
* @param {string} groupeNom
* @param {errorCallback} next
* Ajoute un groupe à la personne, comme membre
* @param {EntityPersonne} personne
* @param {EntityGroupe} groupe
* @param {personneCallback} next appelé avec personne sauvegardé
* @memberOf $personneRepository
*/
function addGroupe (personne, groupeNom, next) {
const oid = personne.oid
if (!oid) return next(new Error('Impossible d’ajouter un groupe à une personne sans oid'))
function ajouterGroupesMembre (personne, groupe, next) {
if (!personne || !personne.oid || !groupe || !groupe.oid) return next(new Error('paramètres invalides'))
const {oid} = personne
const {nom} = groupe
if (!Array.isArray(personne.groupesMembre)) personne.groupesMembre = []
if (personne.groupesMembre.includes(groupeNom)) return next(Error(`La personne ${oid} était déjà membre du groupe ${groupeNom}`))
// on veut une EntityPersonne si on en a pas déjà une (faudra la sauvegarder)
const entityPersonne = personne.store ? personne : EntityPersonne.create(personne)
// faut regarder si le groupe existe
flow().seq(function () {
$groupeRepository.loadByNom(groupeNom, this)
}).seq(function (groupe) {
if (groupe) return this(null, groupe)
// sinon on le crée
const newGroupe = {
nom: groupeNom,
gestionnaires: [oid]
}
EntityGroupe.create(newGroupe).store(this)
}).seq(function (groupe) {
entityPersonne.groupesMembre.push(groupe.nom)
entityPersonne.store(this)
}).seq(function (personneBdd) {
// on mute le groupesMembre du personne passé en argument
personne.groupesMembre = personneBdd.groupesMembre
next()
}).catch(next)
if (personne.groupesMembre.includes(nom)) {
console.error(Error(`La personne ${oid} était déjà membre du groupe ${nom}`))
return next(null, personne)
}
personne.groupesMembre.push(nom)
personne.store(next)
}
/**
......@@ -299,6 +282,25 @@ module.exports = function (component) {
updateMembres(0)
}
/**
* Retire un groupe à la personne, comme membre,
* @param {EntityPersonne} personne
* @param {EntityGroupe} groupe
* @param {personneCallback} next appelé avec personne sauvegardé
* @memberOf $personneRepository
*/
function retirerGroupesMembre (personne, groupe, next) {
if (!personne || !personne.oid || !groupe || !groupe.oid) return next(new Error('paramètres invalides'))
const {oid} = personne
const {nom} = groupe
if (!Array.isArray(personne.groupesMembre) || !personne.groupesMembre.includes(nom)) {
log.error(Error(`La personne ${oid} n’était pas membre du groupe ${nom}`))
return next(null, personne)
}
personne.groupesMembre = personne.groupesMembre.filter(groupeNom => groupeNom !== nom)
personne.store(next)
}
/**
* Enregistre une personne en bdd (et met à jour le cache)
* @param {EntityPersonne} personne
......@@ -356,7 +358,7 @@ module.exports = function (component) {
* @service $personneRepository
*/
const $personneRepository = {
addGroupe,
ajouterGroupesMembre,
countAbonnes,
countMembres,
delete: deletePersonne,
......@@ -367,6 +369,7 @@ module.exports = function (component) {
loadMembres,
removeGroup,
renameGroup,
retirerGroupesMembre,
save,
updateOrCreate
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment