Better error handling during team member invitation
Now tells you if you have already added this member / invite is pending. Behaviour a little bit more predictable during team editting.
This commit is contained in:
		
							parent
							
								
									9f12ccf5c1
								
							
						
					
					
						commit
						5d3e39ea6e
					
				
					 4 changed files with 50 additions and 18 deletions
				
			
		|  | @ -62,7 +62,8 @@ export default function EditTeamModal(props) { | ||||||
|                     <td className="text-right py-2 px-1"> |                     <td className="text-right py-2 px-1"> | ||||||
|                       {member.email !== session.user.email && |                       {member.email !== session.user.email && | ||||||
|                       <button |                       <button | ||||||
|                         onClick={() => removeMember(member)} |                         type="button" | ||||||
|  |                         onClick={(e) => removeMember(member)} | ||||||
|                         className="btn-sm text-xs bg-transparent px-3 py-1 rounded ml-2"> |                         className="btn-sm text-xs bg-transparent px-3 py-1 rounded ml-2"> | ||||||
|                         <UserRemoveIcon className="text-red-400 group-hover:text-gray-500 flex-shrink-0 -mt-1 mr-1 h-4 w-4 inline"/> |                         <UserRemoveIcon className="text-red-400 group-hover:text-gray-500 flex-shrink-0 -mt-1 mr-1 h-4 w-4 inline"/> | ||||||
|                       </button> |                       </button> | ||||||
|  | @ -76,20 +77,20 @@ export default function EditTeamModal(props) { | ||||||
|             <div className="mb-4 border border-red-400 rounded p-2 px-4"> |             <div className="mb-4 border border-red-400 rounded p-2 px-4"> | ||||||
|               <p className="block text-sm font-medium text-gray-700">Tick the box to disband this team.</p> |               <p className="block text-sm font-medium text-gray-700">Tick the box to disband this team.</p> | ||||||
|               <label className="mt-1"> |               <label className="mt-1"> | ||||||
|                 <input type="checkbox" name="title" id="title" onChange={(e) => setCheckedDisbandTeam(e.target.checked)} required className="shadow-sm mr-2 focus:ring-blue-500 focus:border-blue-500  sm:text-sm border-gray-300 rounded-md" /> |                 <input type="checkbox" onChange={(e) => setCheckedDisbandTeam(e.target.checked)} className="shadow-sm mr-2 focus:ring-blue-500 focus:border-blue-500  sm:text-sm border-gray-300 rounded-md" /> | ||||||
|                 Disband this team |                 Disband this team | ||||||
|               </label> |               </label> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|           <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> |           <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> | ||||||
|             {!checkedDisbandTeam && <button type="submit" className="btn btn-primary"> |             {/*!checkedDisbandTeam && <button type="submit" className="btn btn-primary"> | ||||||
|               Update |               Update | ||||||
|             </button>} |             </button>*/} | ||||||
|             {checkedDisbandTeam && <button onClick={deleteTeam} className="btn bg-red-700 rounded text-white px-2 font-medium text-sm"> |             {checkedDisbandTeam && <button onClick={deleteTeam} className="btn bg-red-700 rounded text-white px-2 font-medium text-sm"> | ||||||
|               Disband Team |               Disband Team | ||||||
|             </button>} |             </button>} | ||||||
|             <button onClick={props.onExit} type="button" className="btn btn-white mr-2"> |             <button onClick={props.onExit} type="button" className="btn btn-white mr-2"> | ||||||
|               Cancel |               Close | ||||||
|             </button> |             </button> | ||||||
|           </div> |           </div> | ||||||
|         </form> |         </form> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,22 @@ | ||||||
| import {useEffect, useState} from "react"; | import { UsersIcon } from "@heroicons/react/outline"; | ||||||
| import {UsersIcon} from "@heroicons/react/outline"; | import { useState } from "react"; | ||||||
| 
 | 
 | ||||||
| export default function MemberInvitationModal(props) { | export default function MemberInvitationModal(props) { | ||||||
| 
 | 
 | ||||||
|  |   const [ errorMessage, setErrorMessage ] = useState(''); | ||||||
|  | 
 | ||||||
|  |   const handleError = async (res) => { | ||||||
|  | 
 | ||||||
|  |     const responseData = await res.json(); | ||||||
|  | 
 | ||||||
|  |     if (res.ok === false) { | ||||||
|  |       setErrorMessage(responseData.message); | ||||||
|  |       throw new Error(responseData.message); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return responseData; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const inviteMember = (e) => { |   const inviteMember = (e) => { | ||||||
| 
 | 
 | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|  | @ -19,7 +33,9 @@ export default function MemberInvitationModal(props) { | ||||||
|       headers: { |       headers: { | ||||||
|         'Content-Type': 'application/json' |         'Content-Type': 'application/json' | ||||||
|       } |       } | ||||||
|     }).then(props.onExit); |     }).then(handleError).then(props.onExit).catch( (e) => { | ||||||
|  |       // do nothing.
 | ||||||
|  |     }); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return (<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> |   return (<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> | ||||||
|  | @ -60,6 +76,7 @@ export default function MemberInvitationModal(props) { | ||||||
|               </label> |               </label> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |           {errorMessage && <p className="text-red-700 text-sm"><span class="font-bold">Error: </span>{errorMessage}</p>} | ||||||
|           <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> |           <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> | ||||||
|             <button type="submit" className="btn btn-primary"> |             <button type="submit" className="btn btn-primary"> | ||||||
|               Invite |               Invite | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import type { NextApiRequest, NextApiResponse } from 'next'; | import type { NextApiRequest, NextApiResponse } from 'next'; | ||||||
| import prisma from '../../lib/prisma'; | import prisma from '../../lib/prisma'; | ||||||
| import {getSession} from "next-auth/client"; | import {getSession} from "next-auth/client"; | ||||||
| import {create} from "domain"; |  | ||||||
| 
 | 
 | ||||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||||||
| 
 | 
 | ||||||
|  | @ -13,6 +12,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (req.method === "POST") { |   if (req.method === "POST") { | ||||||
|  | 
 | ||||||
|  |     // TODO: Prevent creating a team with identical names?
 | ||||||
|  | 
 | ||||||
|     const createTeam = await prisma.team.create({ |     const createTeam = await prisma.team.create({ | ||||||
|       data: { |       data: { | ||||||
|         name: req.body.name, |         name: req.body.name, | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   if (!team) { |   if (!team) { | ||||||
|     return res.status(404).json({message: "Unable to find team to invite user to."}); |     return res.status(404).json({message: "Invalid team"}); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const invitee = await prisma.user.findFirst({ |   const invitee = await prisma.user.findFirst({ | ||||||
|  | @ -34,17 +34,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   if (!invitee) { |   if (!invitee) { | ||||||
|     return res.status(404).json({message: "Missing user, currently unsupported."}); |     return res.status(400).json({ | ||||||
|  |       message: `Invite failed because there is no corresponding user for ${req.body.usernameOrEmail}`}); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // create provisional membership
 |   // create provisional membership
 | ||||||
|   const createMembership = await prisma.membership.create({ |   try { | ||||||
|     data: { |     const createMembership = await prisma.membership.create({ | ||||||
|       teamId: parseInt(req.query.team), |       data: { | ||||||
|       userId: invitee.id, |         teamId: parseInt(req.query.team), | ||||||
|       role: req.body.role, |         userId: invitee.id, | ||||||
|     }, |         role: req.body.role, | ||||||
|   }); |       }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   catch (err) { | ||||||
|  |     if (err.code === "P2002") { // unique constraint violation
 | ||||||
|  |       return res.status(409).json({ | ||||||
|  |         message: 'This user is a member of this team / has a pending invitation.', | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       throw err; // rethrow
 | ||||||
|  |     } | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   // inform user of membership by email
 |   // inform user of membership by email
 | ||||||
|   if (req.body.sendEmailInvitation) { |   if (req.body.sendEmailInvitation) { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Alex van Andel
						Alex van Andel