Commit 5357b195 authored by Daniel Caillibaud's avatar Daniel Caillibaud

refacto ajout de gestionnaire avec pid+nom ok

parent 57944caa
......@@ -39,10 +39,7 @@ const saveGroupeAction = (groupe, isNew) => ({
}
})
export const saveGroupe = (
groupe,
success = () => {}
) => dispatch => {
export const saveGroupe = (groupe, success) => dispatch => {
const requestSuccess = responseGroup => {
dispatch(
addNotification({
......
......@@ -20,6 +20,7 @@
* @returns {routeGetter}
*/
const makeGetter = (getRoute, argsNeeded = []) => (params, baseUrl) => {
if (argsNeeded.length && !params) throw Error(`Il faut passer les paramètres ${argsNeeded.join(' et ')} pour obtenir cette route`)
argsNeeded.forEach(arg => {
if (params[arg] === undefined) throw Error(`${arg} est un argument obligatoire pour cette route`)
// les autres valeurs falsy seront castées en string dans l'url
......@@ -37,16 +38,16 @@ const liste = ({search}) => `liste?${search}`
*/
export const getRessourceListUrl = makeGetter(liste, ['search'])
const personneByOid = ({oid}) => `personne/byOid/${oid}`
const checkPid = ({nom, pid}) => `personne/checkPid?nom=${encodeURIComponent(nom)}&pid=${pid}`
/**
* Personne d'après son oid
* @type {routeGetter}
* @param {object} urlParams
* @param {string} urlParams.oid
* @param {string} urlParams.pid
* @param {string} [baseUrl]
* @returns {string}
*/
export const getPersonneByOidUrl = makeGetter(personneByOid, ['oid'])
export const getCheckPidUrl = makeGetter(checkPid, ['nom', 'pid'])
const groupesOuverts = () => `groupes/ouverts`
/**
......
......@@ -8,12 +8,13 @@ const InputField = ({
className,
label,
name,
placeholder,
disabled,
type = 'text'
}) => (
<Field
className={className}
placeholder={label}
placeholder={placeholder || label}
name={name}
component={Input}
type={type}
......@@ -24,9 +25,21 @@ const InputField = ({
InputField.propTypes = {
className: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
name: PropTypes.string,
disabled: PropTypes.bool,
type: PropTypes.string
}
/**
* Composant d'input avec les props classiques
* - [className] une ou des classes css
* - [label] un label qui sera affiché dans un <label> lié au field
* - [info] un éventuel complément de label (affiché dans un <i>)
* - name le nom de l'input (variable du form)
* mais aussi
* - [type=text]
* - [disabled=false]
* - [placeholder=${label}]
*/
export default addLabel(InputField)
......@@ -27,6 +27,7 @@ const addLabel = (WrappedComponent, hasForAttr = true) => {
}
}
// pour le debug, cf https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging
AddLabel.displayName = `addLabel(${getDisplayName(WrappedComponent)})`
AddLabel.propTypes = {
......
import {push} from 'connected-react-router'
import {debounce} from 'lodash'
import PropTypes from 'prop-types'
import React, {Fragment} from 'react'
import {components} from 'react-select'
import {withProps} from 'recompose'
import {formValues, reduxForm} from 'redux-form'
import {GET} from '../../utils/httpMethods'
import {
SwitchField,
InputField,
TextareaField,
SelectField
TextareaField
} from '../fields'
import {saveGroupe} from '../../actions/groupes'
import groupesLoader from './hoc/groupesLoader'
import {getPersonneByOidUrl} from '../../apiRoutes'
import onSubmitFail from '../../utils/onSubmitFail'
import validate from '../../utils/groupeValidate'
import validate, {asyncBlurFields, asyncValidate} from '../../utils/groupeValidate'
const {MultiValueRemove: DefaultMultiValueRemove} = components
const MultiValueRemove = ({
data,
...props
}) => (data.isUndeletable ? null : (
<DefaultMultiValueRemove
data={data}
{...props}
/>
))
MultiValueRemove.propTypes = {
data: PropTypes.shape({})
}
const debouncedGET = debounce((input, setOptions) => {
GET(getPersonneByOidUrl({oid: input}))
.then(({user}) => {
if (user === null) {
return setOptions([])
}
const {oid, prenom, nom} = user
return setOptions([
{
value: oid,
label: `${prenom} ${nom} (${oid})`
}
])
})
.catch(error => {
console.error(error)
setOptions([])
})
}, 500)
const emptyInputRe = /^\s*$/
const getOptions = (input, setOptions) => {
if (emptyInputRe.test(input)) return setOptions([])
debouncedGET(input, setOptions)
}
const LabelGestionnaires = () => (
<Fragment>
Ajouter des gestionnaires
<span className="remarque"> (saisir lidentifiant dun utilisateur, ATTENTION lajout est irrévocable)</span>
</Fragment>
)
// Cf versions avant 2019-05-20 pour l'autocomplete MultiValueRemove
// (ajout de gestionnaires, qu'on a viré car ça permettait d'interroger la base user
// en itérant sur sesasso/NN, mais le composant fonctionnait très bien)
/**
* Formulaire d'édition de groupe
* Doit être dans un redux-form
* @type {PureComponent}
* @param {object} props
* @param {boolean} props.detailed
*/
const GroupeEdition = ({
initialValues: {oid, gestionnaires},
initialValues: {oid, gestionnairesNames},
handleSubmit,
isOuvert,
isPublic,
......@@ -115,18 +63,22 @@ const GroupeEdition = ({
: 'il faut être membre pour suivre les publications du groupe'
})</span>
</div>
<ul>
Gestionnaires :
</ul>
<SelectField
label={<LabelGestionnaires />}
components={{ MultiValueRemove }}
placeholder="Saisir un oid"
name="gestionnaires"
loadOptions={getOptions}
isMulti
isClearable={false}
/>
<div>
<label className="field">Gestionnaire(s)</label>
<ul>
{gestionnairesNames.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
<label className="field">Ajouter un gestionnaire <i>(en le faisant vous vous engagez à lui avoir demandé son avis avant, toutes les modifications de gestionnaires sont enregistrées)</i></label>
<p>ATTENTION : lajout sera effectif lors du clic sur « Modifier » et il est irrévocable.</p>
<div className="grid-2">
<InputField name="newGestionnairePid" label="Son identifiant" placeholder="celui qui apparaît dans « mes informations personnelles »"/>
<InputField name="newGestionnaireNom" label="Son nom" placeholder="Entrez également le nom (insensible à la casse)" />
</div>
</div>
</fieldset>
<hr />
<div className="buttons-area">
......@@ -150,50 +102,42 @@ GroupeEdition.propTypes = {
isPublic: PropTypes.bool
}
// fourni une prop initialValues (en le passant à withProps)
const getInitialValues = ({
// tous les groupes chargés dans le store (appel de groupes/perso, fourni par le hoc groupesLoader)
groupes,
// le nom du groupe édité d'après l'url
match: {params: {groupe: groupeNom}},
// vient de la session (via ensureLogged appelé par groupesLoader)
personne: {
oid,
nom,
prenom
}
}) => {
// valeurs par défaut à la création
const newGroupe = {
ouvert: false,
public: true,
gestionnairesNames: [`${prenom} ${nom}`]
}
let groupe
if (groupeNom) {
groupe = groupes[groupeNom] || {
ouvert: false,
public: true,
gestionnaires: [oid],
gestionnairesNames: [`${prenom} ${nom}`],
nom: groupeNom
if (groupes[groupeNom]) {
// groupe existant
groupe = groupes[groupeNom]
} else {
// un nouveau groupe que l'on vient de créer, pas encore dans le state
groupe = {...newGroupe}
groupe.nom = groupeNom
}
} else {
groupe = {
ouvert: false,
public: true,
gestionnaires: [oid],
gestionnairesNames: [`${prenom} ${nom}`]
}
groupe = newGroupe
}
const {gestionnaires, gestionnairesNames, ...others} = groupe
const gestionnairesItems = gestionnaires.map((oid, index) => ({
value: oid,
label: `${gestionnairesNames[index]} (${oid})`,
isUndeletable: true
}))
const initialValues = {
...others,
gestionnaires: gestionnairesItems
}
return {initialValues}
return {initialValues: groupe}
}
const onSubmit = ({gestionnaires, ...others}, dispatch) => {
const groupe = {
...others,
gestionnaires: gestionnaires.map(({value}) => value)
}
const onSubmit = ({gestionnairesNames, ...groupe}, dispatch) => {
const onSaveSuccess = () => dispatch(push('/groupes/perso'))
const action = saveGroupe(groupe, onSaveSuccess)
dispatch(action)
......@@ -201,12 +145,16 @@ const onSubmit = ({gestionnaires, ...others}, dispatch) => {
const formDefinition = {
form: 'groupe-edition',
asyncValidate,
asyncBlurFields,
onSubmit,
validate,
onSubmitFail
}
const propsFromForm = {
// le mapping props => values que formValues apporte
// le render en a besoin dynamiquement
const formValuesToProps = {
// public est un mot clé js, on préfixe avec is
isPublic: 'public',
// pour ouvert aussi par cohérence
......@@ -217,6 +165,6 @@ const propsFromForm = {
export default groupesLoader(
withProps(getInitialValues)(
reduxForm(formDefinition)(
formValues(propsFromForm)(GroupeEdition))
formValues(formValuesToProps)(GroupeEdition))
)
)
const groupesReducer = (state = null, {type, payload}) => {
switch (type) {
case 'LOAD_GROUPES':
// c'est l'init du state "groupes" géré ici
return payload.groupes
case 'DELETE_GROUPE': {
......@@ -16,7 +17,7 @@ const groupesReducer = (state = null, {type, payload}) => {
case 'SAVE_GROUPE':
case 'JOIN_GROUPE':
case 'FOLLOW_GROUPE': {
if (state === null) { return state }
if (state === null) return state
const {groupe} = payload
const {nom} = groupe
......
......@@ -6,6 +6,11 @@ import session from './session'
import ressource from './ressource'
import groupes from './groupes'
/**
* Combine tous nos reducers
* @param history
* @return {Reducer<any>} le reducer à passer au createStore de redux
*/
const createRootReducer = history => combineReducers({
form,
groupes,
......
import validate from './validate'
import {addNotification} from 'client-react/actions/notifications'
import {GET} from 'client-react/utils/httpMethods'
import {getCheckPidUrl} from 'client-react/apiRoutes'
import {looksLikePid} from 'utils/validators'
const requiredFields = ['nom']
const groupeValidate = validate({requiredFields})
export default groupeValidate
const groupeValidate = validate({
requiredFields
})
export const asyncBlurFields = ['newGestionnairePid', 'newGestionnaireNom']
export default groupeValidate
// un objet pour stocker les appels déjà faits, pour éviter de le refaire plusieurs fois
// sinon on a une notif à chaque blur d'un des deux champs même si on ne les modifie pas
const alreadyCalled = {}
/**
* Appelé au blur sur un des asyncBlurFields (si la validation sync est ok)
* @see https://redux-form.com/6.0.5/examples/asyncvalidation/
* @param {Object} values Les values du form GroupeEdition
* @return {Promise}
*/
export const asyncValidate = (values, dispatch) => {
// on regarde d'abord si les deux champs qui nous intéressent sont non-vide
if (asyncBlurFields.every(key => !values[key])) return Promise.resolve()
const errors = {}
asyncBlurFields.forEach(key => {
if (!values[key] || !values[key].trim()) errors[key] = 'Champ requis pour ajouter un gestionnaire'
})
if (values.newGestionnairePid && !looksLikePid(values.newGestionnairePid)) errors.newGestionnairePid = 'identifiant invalide'
if (Object.keys(errors).length) return Promise.reject(errors)
const pid = values.newGestionnairePid
const nom = values.newGestionnaireNom
if (alreadyCalled[pid] && alreadyCalled[pid][nom]) return Promise.resolve()
const url = getCheckPidUrl({pid, nom})
return GET(url)
.then(({nom: realNom, prenom}) => {
// on a pas besoin de récupérer l'oid car il sera revérifié coté serveur avant l'ajout
if (!alreadyCalled[pid]) alreadyCalled[pid] = {}
alreadyCalled[pid][nom] = true
dispatch(
addNotification({
level: 'info',
message: `${prenom} ${realNom} a bien l'identifiant (${pid})`
})
)
return Promise.resolve()
}).catch((error) => {
console.error(error)
const errors = {newGestionnairePid: error.message}
return Promise.reject(errors)
})
}
......@@ -6,7 +6,7 @@ const errorsMessage = 'La sauvegarde a échoué car des champs contiennent des e
const ArrayFrom = Array.prototype.slice
const onSubmitFail = (_, dispatch, error) => {
const onSubmitFail = (errors, dispatch, error) => {
if (error) {
dispatch(addNotification({
level: 'error',
......@@ -15,7 +15,10 @@ const onSubmitFail = (_, dispatch, error) => {
throw error
}
// pourquoi aller chercher les messages d'erreur dans le dom alors qu'ils sont déjà dans l'objet errors ?
const elements = ArrayFrom.call(document.querySelectorAll('.validation-error'))
// à creuser…
// console.log('onSubmitFail avec les erreurs', errors, 'qui devraient déjà être affichées dans le dom dans les éléments', elements)
if (elements.length) {
const element = elements[0]
if (element && element.scrollIntoView) element.scrollIntoView(false)
......
......@@ -63,26 +63,29 @@ module.exports = function (component) {
const data = context.post
const myOid = $accessControl.getCurrentUserOid(context)
const sendInternalError = (error) => $json.sendKo(context, error)
// checks de base
if (!myOid) return $json.denied(context, 'Vous devez être authentifié pour créer des groupes')
if (!data.nom) return $json.sendKo(context, 'Impossible de créer un groupe sans nom')
// on init les éventuelles valeurs manquantes par nos valeurs par défaut
data.nom = data.nom.trim()
if (!data.oid && !$accessControl.hasGenericPermission('createGroupe', context)) return $json.denied(context, 'Vous n’avez pas les droits suffisants pour créer un groupe')
// cas modif
if (data.oid) {
// update, faut aller voir en base
flow().seq(function () {
EntityGroupe.match('oid').equals(data.oid).grabOne(this)
}).seq(function (groupeBdd) {
flow().seq(function () {
if (data.oid) return EntityGroupe.match('oid').equals(data.oid).grabOne(this)
// à 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)
}).seq(function (groupeBdd) {
const nextStep = this
if (data.oid) {
if (!groupeBdd) return $json.notFound(context, `Le groupe d’identifiant ${data.oid} n’existe pas`)
if (!isManaged(context, groupeBdd)) return $json.denied(context, 'Vous n’êtes pas gestionnaire de ce groupe et ne pouvez pas le modifier')
// si le nom change pas on passe à la suite
const oldName = groupeBdd.nom
const newName = data.nom
if (!newName || newName === oldName) return this(null, groupeBdd)
if (!newName || newName === oldName) return nextStep(null, groupeBdd)
// mais sinon faut répercuter partout
const nextStep = this
flow().seq(function () {
$personneRepository.renameGroup(oldName, newName, this)
}).seq(function () {
......@@ -94,58 +97,53 @@ module.exports = function (component) {
groupeBdd.nom = newName
nextStep(null, groupeBdd)
}).catch(sendInternalError)
// on peut passer aux autres propriétés du groupe
}).seq(function (groupeBdd) {
// les booléens
if (data.hasOwnProperty('ouvert')) groupeBdd.ouvert = Boolean(data.ouvert)
if (data.hasOwnProperty('public')) groupeBdd.public = Boolean(data.public)
// description
if (data.hasOwnProperty('description')) groupeBdd.description = data.description
// et les gestionnaires
if (Array.isArray(data.gestionnaires) && data.gestionnaires.length) {
// le seul gestionnaire qu'on peut enlever est soi-même
if (!data.gestionnaires.includes(myOid)) groupeBdd.gestionnaires = groupeBdd.gestionnaires.filter(oid => oid !== myOid)
// le reste ne peut être que des ajouts
data.gestionnaires.forEach(g => {
if (!groupeBdd.gestionnaires.includes(g)) groupeBdd.gestionnaires.push(g)
})
// fin rename group
} else {
if (groupeBdd) {
return $json.sendKo(context, `Le groupe « ${data.nom} » existe déjà`)
}
groupeBdd.store(this)
}).seq(function (groupe) {
addGestionnairesNames(context, groupe, this)
}).seq(function (groupe) {
$json.sendOk(context, groupe)
}).catch(sendInternalError)
// cas nouveau groupe
} else {
if (!$accessControl.hasGenericPermission('createGroupe', context)) {
return $json.denied(context, 'Vous n’avez pas les droits suffisants pour créer un groupe')
const groupe = {
nom: data.nom,
gestionnaires: [myOid] // à la création c'est imposé
}
nextStep(null, EntityGroupe.create(groupe))
}
const gestionnaires = data.gestionnaires || []
if (!gestionnaires.includes(myOid)) gestionnaires.push(myOid)
const groupe = {
nom: data.nom,
ouvert: data.hasOwnProperty('ouvert') ? Boolean(data.ouvert) : false,
public: data.hasOwnProperty('public') ? Boolean(data.public) : true,
description: data.description,
gestionnaires
}
// il manque une clé unique sur le nom?
EntityGroupe.create(groupe).store((error, groupe) => {
if (error) return $json.sendKo(context, error)
// on peut passer aux autres propriétés du groupe
}).seq(function (groupe) {
// les booléens
if (data.hasOwnProperty('ouvert')) groupe.ouvert = Boolean(data.ouvert)
if (data.hasOwnProperty('public')) groupe.public = Boolean(data.public)
// description
if (data.hasOwnProperty('description')) groupe.description = data.description
// et les gestionnaires à ajouter éventuellement, on shunt si les deux champs sont vides
// (si seul l'un des deux est vide ça tentera le chargement et renverra une erreur)
if (!data.newGestionnairePid && !data.newGestionnaireNom) return this(null, groupe)
// on va chercher l'oid de ce nouveau gestionnaire
$personneRepository.loadByPidAndNom(data.newGestionnairePid, data.newGestionnaireNom, (error, personne) => {
if (error) return this(error)
if (!groupe.gestionnaires.includes(personne.oid)) groupe.gestionnaires.push(personne.oid)
this(null, groupe)
// else faudrait ajouter un warning, mais le front redirige s'il récupère du 200, on laisse tomber
})
}).seq(function (groupe) {
groupe.store(this)
}).seq(function (groupe) {
// à la création, faut imposer joinAndFollow + ajouter les gestionnairesNames
// lors d'une modif faut ajouter les gestionnairesNames s'ils ont changé
// vu que cette étape est assez rare on simplifie le back en renvoyant toujours tout
// (le front pourra évoluer comme il veut sans répercussions ici)
if (data.oid) return this(null, groupe)
flow().seq(function () {
joinAndFollowGroup(context, groupe.nom, this)
}).seq(function () {
addGestionnairesNames(context, groupe, this)
}).seq(function () {
$json.sendOk(context, groupe)
}).catch(sendInternalError)
joinAndFollowGroup(context, groupe.nom, (error) => {
if (error) return this(error)
this(null, groupe)
})
}
}).seq(function (groupe) {
addGestionnairesNames(context, groupe, this)
}).seq(function (groupe) {
$json.sendOk(context, groupe)
}).catch(sendInternalError)
})
controller.options('', optionsOk)
......
......@@ -32,13 +32,14 @@
'use strict'
const flow = require('an-flow')
const {looksLikePid} = require('../../utils/validators')
module.exports = function (component) {
/**
* Controleurs de la route /api/personne/
* @Controller controllerApiPersonne
*/
component.controller('api/personne', function (EntityPersonne, $personneRepository, $accessControl, $json, $groupeRepository, $session) {
component.controller('api/personne', function (EntityPersonne, $accessControl, $json, $groupeRepository, $personneRepository, $session) {
// service $auth qu'on ne peut pas mettre en dépendance car le component auth
// est chargé après ce component personne (il utilise ses services)
let $auth
......@@ -145,23 +146,22 @@ module.exports = function (component) {
})
/**
* Récupère le nom d'un utilisateur
* depuis son oid
* @route GET /api/personne/byOid/:oid
* Vérifie qu'un pid correspond à un nom et retourne un user: {oid,pid,nom,prenom} ou un message d'erreur
* @route GET /api/personne/checkPid?pid=xxx&nom=yyy
*/
this.get('byOid/:oid', function (context) {
this.get('checkPid', function (context) {
const myOid = $accessControl.getCurrentUserOid(context)
if (!myOid) return $json.denied(context, 'Vous devez être authentifié pour chercher des utilisateurs')
const {oid} = context.arguments
flow().seq(function () {
$personneRepository.load(oid, this)
}).seq(function (personne) {
if (!personne) return $json.sendOk(context, {user: null})
const {nom, oid, prenom} = personne
$json.sendOk(context, {user: {nom, oid, prenom}})
}).catch(function (error) {
$json.sendKo(context, error)
const {nom: askedNom, pid: askedPid} = context.get
if (!looksLikePid(askedPid)) return $json.sendKo(context, 'Paramètre pid invalide')
if (!askedNom) return $json.sendKo(context, 'Paramètre nom manquant')
$personneRepository.loadByPidAndNom(askedPid, askedNom, (error, personne) => {
if (error) return $json.sendKo(context, error)
const {nom, oid, pid, prenom} = personne
$json.sendOk(context, {nom, oid, pid, prenom})
})
})
})
......
......@@ -138,7 +138,7 @@ module.exports = function (component) {
// méthodes exportées
/**
* Ajoute gestionnairesNames au groupe
* Ajoute gestionnairesNames au groupe (en allant chercher les noms en bdd)
* @param {Context} context
* @param {Groupe} groupe
* @param {groupeCallback} next
......
......@@ -162,8 +162,6 @@ module.exports = function (component) {
flow().seq(function () {
// grab auteurs
$personneRepository.loadByPids(pids, this)
// le 2e argument (missing) passé à la cb sera ignoré par seqEach, qui ne traite que l'array
// qu'on lui passe en 1er argument
}).seqEach(function (personne, index) {
if (personne) {
retour[personne.pid] = {
......
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