import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { TEAM_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/teams'; import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations'; import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@documenso/ui/primitives/dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { zodResolver } from '@hookform/resolvers/zod'; import { Trans, useLingui } from '@lingui/react/macro'; import { TeamMemberRole } from '@prisma/client'; import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { InfoIcon, UserPlusIcon } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Link } from 'react-router'; import { match } from 'ts-pattern'; import { z } from 'zod'; import { OrganisationMemberInviteDialog } from '~/components/dialogs/organisation-member-invite-dialog'; import { type OrganisationMemberOption, OrganisationMembersMultiSelectCombobox, } from '~/components/general/organisation-members-multiselect-combobox'; import { useCurrentTeam } from '~/providers/team'; export type TeamMemberCreateDialogProps = { trigger?: React.ReactNode; } & Omit; const ZAddTeamMembersFormSchema = z.object({ members: z.array( z.object({ organisationMemberId: z.string(), teamRole: z.nativeEnum(TeamMemberRole), }), ), }); type TAddTeamMembersFormSchema = z.infer; export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDialogProps) => { const [open, setOpen] = useState(false); const [step, setStep] = useState<'SELECT' | 'MEMBERS'>('SELECT'); const [inviteDialogOpen, setInviteDialogOpen] = useState(false); const [selectedMembers, setSelectedMembers] = useState([]); const prevInviteDialogOpenRef = useRef(false); const { t } = useLingui(); const { toast } = useToast(); const team = useCurrentTeam(); const organisation = useCurrentOrganisation(); const utils = trpc.useUtils(); const canInviteOrganisationMembers = canExecuteOrganisationAction( 'MANAGE_ORGANISATION', organisation.currentOrganisationRole, ); const form = useForm({ resolver: zodResolver(ZAddTeamMembersFormSchema), defaultValues: { members: [], }, }); const { mutateAsync: createTeamMembers } = trpc.team.member.createMany.useMutation(); // Lightweight count-only query for org members not already on this team. // Powers the "no available members" empty state. const availableMemberCountQuery = trpc.organisation.member.find.useQuery({ organisationId: team.organisationId, perPage: 1, excludeTeamId: team.id, }); const hasNoAvailableMembers = !availableMemberCountQuery.isLoading && (availableMemberCountQuery.data?.count ?? 0) === 0; const onFormSubmit = async ({ members }: TAddTeamMembersFormSchema) => { if (members.length === 0) { if (hasNoAvailableMembers && canInviteOrganisationMembers) { setInviteDialogOpen(true); return; } // Don't show error if on SELECT step - the disabled Next button already communicates this if (step === 'SELECT') { return; } toast({ title: t`No members selected`, description: t`Please select at least one member to add to the team.`, variant: 'destructive', }); return; } try { await createTeamMembers({ teamId: team.id, organisationMembers: members, }); toast({ title: t`Success`, description: t`Team members have been added.`, duration: 5000, }); setOpen(false); } catch { toast({ title: t`An unknown error occurred`, description: t`We encountered an unknown error while attempting to add team members. Please try again later.`, variant: 'destructive', }); } }; useEffect(() => { if (!open) { form.reset(); setStep('SELECT'); setInviteDialogOpen(false); setSelectedMembers([]); } }, [open, form]); // Invalidate queries when invite dialog closes (transitions from true to false) to refresh available members useEffect(() => { if (prevInviteDialogOpenRef.current && !inviteDialogOpen) { void utils.organisation.member.find.invalidate({ organisationId: team.organisationId, }); } prevInviteDialogOpenRef.current = inviteDialogOpen; }, [inviteDialogOpen, utils, team.organisationId]); return ( e.stopPropagation()} asChild> {trigger ?? ( )} {match(step) .with('SELECT', () => ( Add members To be able to add members to a team, you must first add them to the organisation. For more information, please see the{' '} documentation . Select members or groups of members to add to the team. )) .with('MEMBERS', () => ( Add members roles Configure the team roles for each member )) .exhaustive()}
{ if (e.key === 'Enter' && form.getValues('members').length === 0) { e.preventDefault(); if (hasNoAvailableMembers && canInviteOrganisationMembers) { setInviteDialogOpen(true); } // Don't show toast - the disabled Next button already communicates this } }} >
{step === 'SELECT' && ( <> ( Members {hasNoAvailableMembers ? (

No organisation members available

{canInviteOrganisationMembers ? ( To add members to this team, you must first add them to the organisation. ) : ( To add members to this team, they must first be invited to the organisation. Only organisation admins and managers can invite new members — please contact one of them to invite members on your behalf. )}

{canInviteOrganisationMembers && ( Invite organisation members } /> )}
) : ( { setSelectedMembers(members); field.onChange( members.map((member) => ({ organisationMemberId: member.id, teamRole: field.value.find((entry) => entry.organisationMemberId === member.id)?.teamRole || TeamMemberRole.MEMBER, })), ); }} className="w-full bg-background" dataTestId="team-members-picker" /> )}
{!hasNoAvailableMembers && ( <> Select members to add to this team {canInviteOrganisationMembers && (
Can't find someone?{' '} Invite them to the organisation first } />
)} )}
)} /> )} {step === 'MEMBERS' && ( <>
{form.getValues('members').map((member, index) => (
{index === 0 && ( Member )} id === member.organisationMemberId)?.name || ''} />
( {index === 0 && ( Team Role )} )} />
))}
)}
); };