'use client'; import { useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useQuery } from '@tanstack/react-query'; import { Mail, Plus, X } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useAction } from 'next-safe-action/hooks'; import { useFieldArray, useForm } from 'react-hook-form'; import { Alert, AlertDescription } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@kit/ui/dialog'; import { Form, FormControl, FormField, FormItem, FormMessage, } from '@kit/ui/form'; import { If } from '@kit/ui/if'; import { InputGroup, InputGroupAddon, InputGroupInput, } from '@kit/ui/input-group'; import { toast } from '@kit/ui/sonner'; import { Spinner } from '@kit/ui/spinner'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@kit/ui/tooltip'; import { Trans } from '@kit/ui/trans'; import { InviteMembersSchema } from '../../schema/invite-members.schema'; import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions'; import { MembershipRoleSelector } from './membership-role-selector'; import { RolesDataProvider } from './roles-data-provider'; type InviteModel = ReturnType; type Role = string; /** * The maximum number of invites that can be sent at once. * Useful to avoid spamming the server with too large payloads */ const MAX_INVITES = 5; export function InviteMembersDialogContainer({ accountSlug, userRoleHierarchy, children, }: React.PropsWithChildren<{ accountSlug: string; userRoleHierarchy: number; }>) { const [isOpen, setIsOpen] = useState(false); const t = useTranslations('teams'); const { execute, isPending } = useAction(createInvitationsAction, { onSuccess: ({ data }) => { if (data?.success) { toast.success(t('inviteMembersSuccessMessage')); } else { toast.error(t('inviteMembersErrorMessage')); } setIsOpen(false); }, onError: () => { toast.error(t('inviteMembersErrorMessage')); setIsOpen(false); }, }); // Evaluate policies when dialog is open const { data: policiesResult, isLoading: isLoadingPolicies, error: policiesError, } = useFetchInvitationsPolicies({ accountSlug, isOpen }); return (
{(roles) => ( { execute({ accountSlug, invitations: data.invitations, }); }} /> )}
); } function InviteMembersForm({ onSubmit, roles, pending, }: { onSubmit: (data: { invitations: InviteModel[] }) => void; pending: boolean; roles: string[]; }) { const t = useTranslations('teams'); const form = useForm({ resolver: zodResolver(InviteMembersSchema), shouldUseNativeValidation: true, reValidateMode: 'onSubmit', defaultValues: { invitations: [createEmptyInviteModel()], }, }); const fieldArray = useFieldArray({ control: form.control, name: 'invitations', }); return (
{fieldArray.fields.map((field, index) => { const emailInputName = `invitations.${index}.email` as const; const roleInputName = `invitations.${index}.role` as const; return (
{ return ( ); }} /> { return ( { if (role) { form.setValue(field.name, role); } }} /> ); }} />
{ fieldArray.remove(index); form.clearErrors(emailInputName); }} > } /> {t('removeInviteButtonLabel')}
); })}
); } function createEmptyInviteModel() { return { email: '', role: 'member' as Role }; } function useFetchInvitationsPolicies({ accountSlug, isOpen, }: { accountSlug: string; isOpen: boolean; }) { return useQuery({ queryKey: ['invitation-policies', accountSlug], queryFn: async () => { const response = await fetch(`./members/policies`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }, enabled: isOpen, staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, }); }