From 1a56eb0875e1862508648c38bcba05efd21681ba Mon Sep 17 00:00:00 2001 From: Arnaud Ambroselli <31724752+arnaudambro@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:36:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20cr=C3=A9ation=20d'utilisateu?= =?UTF-8?q?rs=20plus=20rapide=20avec=20valeurs=20par=20d=C3=A9faut=20et=20?= =?UTF-8?q?possiblit=C3=A9=20d'enchainer=20les=20uns=20apr=C3=A8s=20les=20?= =?UTF-8?q?autres=20rapidement=20(#1691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(dashboard): création d'utilisateurs plus rapide avec valeurs par défaut et possiblité d'enchainer les uns après les autres rapidement * fix: no username by default --- api/src/controllers/organisation.js | 2 +- api/src/controllers/user.js | 13 +- dashboard/src/components/ChangePassword.js | 6 +- dashboard/src/components/createWrapper.js | 10 - dashboard/src/scenes/auth/reset.js | 13 +- dashboard/src/scenes/place/list.js | 5 +- dashboard/src/scenes/team/list.js | 5 +- dashboard/src/scenes/user/list.js | 311 ++++++++++++--------- 8 files changed, 213 insertions(+), 152 deletions(-) delete mode 100644 dashboard/src/components/createWrapper.js diff --git a/api/src/controllers/organisation.js b/api/src/controllers/organisation.js index 3017b4280..0714c5d25 100644 --- a/api/src/controllers/organisation.js +++ b/api/src/controllers/organisation.js @@ -164,7 +164,7 @@ Un compte Mano pour votre organisation ${organisation.name} vient d'être créé Votre identifiant pour vous connecter à Mano est ${adminUser.email}. Vous pouvez dès à présent vous connecter pour choisir votre mot de passe ici: -https://dashboard-mano.fabrique.social.gouv.fr/auth/reset?token=${token} +https://dashboard-mano.fabrique.social.gouv.fr/auth/reset?token=${token}&newUser=true Vous pourrez ensuite paramétrer votre organisation et commencer à utiliser Mano en suivant ce lien: https://dashboard-mano.fabrique.social.gouv.fr/ diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index 23172fe41..280e730be 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -270,6 +270,7 @@ router.post( catchErrors(async (req, res, next) => { try { z.object({ + name: z.string().optional(), token: z.string().min(1), password: z.string().min(1), }).parse(req.body); @@ -279,8 +280,9 @@ router.post( return next(error); } const { - body: { token, password }, + body: { token, password, name }, } = req; + console.log(req.body); if (!validatePassword(password)) return res.status(400).send({ ok: false, error: passwordCheckError, code: PASSWORD_NOT_VALIDATED }); const user = await User.findOne({ where: { forgotPasswordResetToken: token, forgotPasswordResetExpires: { [Op.gte]: new Date() } } }); @@ -292,6 +294,7 @@ router.post( forgotPasswordResetExpires: null, lastChangePasswordAt: Date.now(), }); + if (name) user.set({ name: sanitizeAll(name) }); await user.save(); return res.status(200).send({ ok: true }); }) @@ -304,7 +307,7 @@ router.post( catchErrors(async (req, res, next) => { try { z.object({ - name: z.string().min(1), + name: z.string().optional(), email: z.preprocess((email) => email.trim().toLowerCase(), z.string().email().optional().or(z.literal(""))), healthcareProfessional: z.boolean(), team: z.array(z.string().regex(looseUuidRegex)), @@ -330,7 +333,7 @@ router.post( }; const prevUser = await User.findOne({ where: { email: newUser.email } }); - if (prevUser) return res.status(400).send({ ok: false, error: "A user already exists with this email" }); + if (prevUser) return res.status(400).send({ ok: false, error: "Un utilisateur existe déjà avec cet email" }); const data = await User.create(newUser, { returning: true }); @@ -348,8 +351,8 @@ router.post( const body = `Bonjour ${data.name} ! Votre identifiant pour vous connecter à Mano est ${data.email}. -Vous pouvez dès à présent vous connecter pour choisir votre mot de passe ici: -https://dashboard-mano.fabrique.social.gouv.fr/auth/reset?token=${token} +Vous pouvez dès à présent vous connecter pour choisir votre nom d'utilisateur et mot de passe ici: +https://dashboard-mano.fabrique.social.gouv.fr/auth/reset?token=${token}&newUser=true NOTE: si le lien ci-dessus est expiré, vous pouvez demander une nouvelle réinitialisation de mot de passe ici: https://dashboard-mano.fabrique.social.gouv.fr Vous pourrez ensuite commencer à utiliser Mano en suivant ce lien: diff --git a/dashboard/src/components/ChangePassword.js b/dashboard/src/components/ChangePassword.js index bf86548d1..287d6e25a 100644 --- a/dashboard/src/components/ChangePassword.js +++ b/dashboard/src/components/ChangePassword.js @@ -100,7 +100,7 @@ const ChangePassword = ({ onSubmit, onFinished, withCurrentPassword, centerButto Confirmez le nouveau mot de passe { const location = useLocation(); const searchParams = new URLSearchParams(location.search); const token = searchParams.get('token'); + const newUser = searchParams.get('newUser') === 'true'; + const [name, setName] = useState(''); if (!token) return ; if (redirect) return ; return (
-

Modifiez votre mot de passe

+

+ {newUser ? "Choisissez un nom d'utilisateur et un mot de passe" : 'Modifiez votre mot de passe'} +

+ {!!newUser && ( +
+ + setName(e.target.value)} /> +
+ )} { return API.post({ path: '/user/forgot_password_reset', body: { token, + name, password: newPassword, }, }); diff --git a/dashboard/src/scenes/place/list.js b/dashboard/src/scenes/place/list.js index e7f14c558..f0667f28e 100644 --- a/dashboard/src/scenes/place/list.js +++ b/dashboard/src/scenes/place/list.js @@ -6,7 +6,6 @@ import { toast } from 'react-toastify'; import { SmallHeader } from '../../components/header'; import ButtonCustom from '../../components/ButtonCustom'; import Loading from '../../components/loading'; -import CreateWrapper from '../../components/createWrapper'; import Table from '../../components/table'; import Search from '../../components/search'; import Page from '../../components/pagination'; @@ -110,7 +109,7 @@ const Create = () => { const setPlaces = useSetRecoilState(placesState); return ( - +
setOpen(true)} @@ -152,7 +151,7 @@ const Create = () => { - +
); }; diff --git a/dashboard/src/scenes/team/list.js b/dashboard/src/scenes/team/list.js index d70e31196..f0ed77eef 100644 --- a/dashboard/src/scenes/team/list.js +++ b/dashboard/src/scenes/team/list.js @@ -7,7 +7,6 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { SmallHeader } from '../../components/header'; import ButtonCustom from '../../components/ButtonCustom'; -import CreateWrapper from '../../components/createWrapper'; import Table from '../../components/table'; import NightSessionModale from '../../components/NightSessionModale'; import { currentTeamState, organisationState, teamsState, userState } from '../../recoil/auth'; @@ -99,7 +98,7 @@ const Create = () => { const onboardingForTeams = !teams.length; return ( - +
setOpen(true)} title="Créer une nouvelle équipe" padding="12px 24px" /> setOpen(false)} size="lg" backdrop="static"> : null} toggle={() => setOpen(false)}> @@ -201,7 +200,7 @@ const Create = () => { - +
); }; diff --git a/dashboard/src/scenes/user/list.js b/dashboard/src/scenes/user/list.js index 7da4f1d93..eabec2681 100644 --- a/dashboard/src/scenes/user/list.js +++ b/dashboard/src/scenes/user/list.js @@ -1,23 +1,19 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Col, FormGroup, Input, Modal, ModalBody, ModalHeader, Row, Label } from 'reactstrap'; import { useHistory } from 'react-router-dom'; -import { Formik } from 'formik'; import { toast } from 'react-toastify'; -import styled from 'styled-components'; import { useRecoilValue } from 'recoil'; +import { teamsState, userState } from '../../recoil/auth'; +import API from '../../services/api'; +import { formatDateWithFullMonth } from '../../services/date'; +import useTitle from '../../services/useTitle'; +import { useLocalStorage } from '../../services/useLocalStorage'; +import { ModalBody, ModalContainer, ModalHeader, ModalFooter } from '../../components/tailwind/Modal'; import { SmallHeader } from '../../components/header'; import SelectTeamMultiple from '../../components/SelectTeamMultiple'; import Loading from '../../components/loading'; -import ButtonCustom from '../../components/ButtonCustom'; import Table from '../../components/table'; -import CreateWrapper from '../../components/createWrapper'; import TagTeam from '../../components/TagTeam'; -import { userState } from '../../recoil/auth'; -import API from '../../services/api'; -import { formatDateWithFullMonth } from '../../services/date'; -import useTitle from '../../services/useTitle'; import SelectRole from '../../components/SelectRole'; -import { useLocalStorage } from '../../services/useLocalStorage'; import { emailRegex } from '../../utils'; const defaultSort = (a, b, sortOrder) => (sortOrder === 'ASC' ? (a.name || '').localeCompare(b.name) : (b.name || '').localeCompare(a.name)); @@ -57,25 +53,24 @@ const List = () => { const [sortBy, setSortBy] = useLocalStorage('users-sortBy', 'createdAt'); const [sortOrder, setSortOrder] = useLocalStorage('users-sortOrder', 'ASC'); - const getUsers = async () => { - const response = await API.get({ path: '/user' }); - if (response.error) return toast.error(response.error); - setUsers(response.data); - setRefresh(false); - }; - const data = useMemo(() => users.sort(sortUsers(sortBy, sortOrder)), [users, sortBy, sortOrder]); useEffect(() => { - getUsers(); - // eslint-disable-next-line react-hooks/exhaustive-deps + API.get({ path: '/user' }).then((response) => { + if (response.error) { + toast.error(response.error); + return false; + } + setUsers(response.data); + setRefresh(false); + }); }, [refresh]); - if (!!refresh) return ; + if (!users.length) return ; return ( <> - {['superadmin', 'admin'].includes(user.role) && setRefresh(true)} />} + {['admin'].includes(user.role) && setRefresh(true)} />} { dataKey: 'teams', render: (user) => { return ( - +
{user.teams.map((t) => ( ))} - +
); }, }, @@ -150,115 +145,179 @@ const List = () => { ); }; -const TeamWrapper = styled.div` - display: grid; - grid-auto-flow: row; - row-gap: 5px; -`; - -const Create = ({ onChange }) => { +const Create = ({ onChange, users }) => { const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const teams = useRecoilValue(teamsState); + const initialState = useMemo(() => { + return { + name: '', + email: '', + role: 'normal', + team: teams.map((t) => t._id), + healthcareProfessional: false, + }; + }, [teams]); + useEffect(() => { + if (!open) setData(initialState); + }, [open, initialState]); + const [data, setData] = useState(initialState); + const handleChange = (event) => { + const target = event.currentTarget || event.target; + const { name, value } = target; + setData((data) => ({ ...data, [name]: value })); + }; + + const handleSubmit = async () => { + try { + if (data.role === 'restricted-access') data.healthcareProfessional = false; + if (!data.email) { + toast.error("L'email est obligatoire"); + return false; + } + if (!emailRegex.test(data.email)) { + toast.error("L'email est invalide"); + return false; + } + if (!data.role) { + toast.error('Le rôle est obligatoire'); + return false; + } + if (!data.team?.length) { + toast.error('Veuillez sélectionner une équipe'); + return false; + } + setIsSubmitting(true); + const { ok } = await API.post({ path: '/user', body: data }); + setIsSubmitting(false); + if (!ok) { + return false; + } + toast.success('Création réussie !'); + onChange(); + setData(initialState); + return true; + } catch (errorCreatingUser) { + console.log('error in creating user', errorCreatingUser); + toast.error(errorCreatingUser.message); + setIsSubmitting(false); + return false; + } + }; + return ( - - setOpen(true)} color="primary" title="Créer un nouvel utilisateur" padding="12px 24px" /> - setOpen(false)} size="lg" backdrop="static"> - setOpen(false)}>Créer un nouvel utilisateur +
+ + setOpen(false)} size="full"> + setOpen(false)} title="Créer de nouveaux utilisateurs" /> - { - const errors = {}; - if (values.role === 'restricted-access') values.healthcareProfessional = false; - if (!values.name) errors.name = 'Le nom est obligatoire'; - if (!values.email) errors.email = "L'email est obligatoire"; - else if (!emailRegex.test(values.email)) errors.email = "L'email est invalide"; - if (!values.role) errors.role = 'Le rôle est obligatoire'; - if (!values.team?.length) errors.team = 'Veuillez sélectionner une équipe'; - return errors; - }} - onSubmit={async (body, actions) => { - try { - const { ok } = await API.post({ path: '/user', body }); - actions.setSubmitting(false); - if (!ok) return; - toast.success('Création réussie !'); - onChange(); - setOpen(false); - } catch (errorCreatingUser) { - console.log('error in creating user', errorCreatingUser); - toast.error(errorCreatingUser.message); - } +
{ + e.preventDefault(); + await handleSubmit(); }}> - {({ values, handleChange, handleSubmit, isSubmitting, errors, touched }) => ( - - -
- - - - {touched.name && errors.name &&
{errors.name}
} -
- - - - - - {touched.email && errors.email &&
{errors.email}
} -
- - - - - - {touched.role && errors.role &&
{errors.role}
} -
- - - - -
- handleChange({ target: { value: teamIds, name: 'team' } })} - value={values.team || []} - colored - inputId="team" - /> - {touched.team && errors.team &&
{errors.team}
} -
-
- - {values.role !== 'restricted-access' && ( - - -
- Un professionnel·le de santé a accès au dossier médical complet des personnes. -
- - )} - -
- - - - - - - )} - +
+
+ + +
+
+ +
+ +
+
+
+ +
+ handleChange({ target: { value: teamIds, name: 'team' } })} + value={data.team} + inputId="team" + name="team" + /> +
+
+ {/*
+ + +
*/} + {data.role !== 'restricted-access' && ( +
+ +
+ Un professionnel·le de santé a accès au dossier médical complet des personnes. +
+
+ )} +
+
+ Utilisateurs existants ({users?.length ?? 0}) +
    + {users.map((user) => { + return ( + +
    {user.name}
    +
    {user.email}
    +
    + ); + })} +
+
+ - - + + + + + + + ); };