diff --git a/apps/remix/app/components/dialogs/team-member-create-dialog.tsx b/apps/remix/app/components/dialogs/team-member-create-dialog.tsx index 8f822cfad..b8e5cfef5 100644 --- a/apps/remix/app/components/dialogs/team-member-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/team-member-create-dialog.tsx @@ -1,10 +1,10 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; 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 } from 'lucide-react'; +import { InfoIcon, UserPlusIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { Link } from 'react-router'; import { match } from 'ts-pattern'; @@ -13,6 +13,7 @@ import { z } from 'zod'; import { TEAM_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/teams'; import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations'; import { trpc } from '@documenso/trpc/react'; +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, @@ -44,6 +45,7 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { useToast } from '@documenso/ui/primitives/use-toast'; +import { OrganisationMemberInviteDialog } from '~/components/dialogs/organisation-member-invite-dialog'; import { useCurrentTeam } from '~/providers/team'; export type TeamMemberCreateDialogProps = { @@ -64,11 +66,14 @@ 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 prevInviteDialogOpenRef = useRef(false); const { t } = useLingui(); const { toast } = useToast(); const team = useCurrentTeam(); + const utils = trpc.useUtils(); const form = useForm({ resolver: zodResolver(ZAddTeamMembersFormSchema), @@ -96,7 +101,29 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi ); }, [organisationMemberQuery, teamMemberQuery]); + const hasNoAvailableMembers = + !organisationMemberQuery.isLoading && avaliableOrganisationMembers.length === 0; + const onFormSubmit = async ({ members }: TAddTeamMembersFormSchema) => { + if (members.length === 0) { + if (hasNoAvailableMembers) { + 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, @@ -123,9 +150,20 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi if (!open) { form.reset(); setStep('SELECT'); + setInviteDialogOpen(false); } }, [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 ?? ( + + )} @@ -149,7 +189,7 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi - + To be able to add members to a team, you must first add them to the organisation. For more information, please see the{' '} @@ -186,7 +226,18 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi .exhaustive()}
- + { + if (e.key === 'Enter' && form.getValues('members').length === 0) { + e.preventDefault(); + if (hasNoAvailableMembers) { + setInviteDialogOpen(true); + } + // Don't show toast - the disabled Next button already communicates this + } + }} + >
{step === 'SELECT' && ( <> @@ -194,46 +245,102 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi control={form.control} name="members" render={({ field }) => ( - + Members - ({ - label: member.name, - value: member.id, - }))} - loading={organisationMemberQuery.isLoading} - selectedValues={field.value.map( - (member) => member.organisationMemberId, - )} - onChange={(value) => { - field.onChange( - value.map((organisationMemberId) => ({ - organisationMemberId, - teamRole: - field.value.find( - (member) => - member.organisationMemberId === organisationMemberId, - )?.teamRole || TeamMemberRole.MEMBER, - })), - ); - }} - className="bg-background w-full" - emptySelectionPlaceholder={t`Select members`} - /> + {hasNoAvailableMembers ? ( +
+
+ +
+

+ No organisation members available +

+

+ + To add members to this team, you must first add them to the + organisation. + +

+ + + Invite organisation members + + } + /> +
+ ) : ( + ({ + label: member.name, + value: member.id, + }))} + loading={organisationMemberQuery.isLoading} + selectedValues={field.value.map( + (member) => member.organisationMemberId, + )} + onChange={(value) => { + field.onChange( + value.map((organisationMemberId) => ({ + organisationMemberId, + teamRole: + field.value.find( + (member) => + member.organisationMemberId === organisationMemberId, + )?.teamRole || TeamMemberRole.MEMBER, + })), + ); + }} + className="w-full bg-background" + emptySelectionPlaceholder={t`Select members`} + /> + )}
- - Select members to add to this team - + {!hasNoAvailableMembers && ( + <> + + Select members to add to this team + + + +
+ +
+ + Can't find someone?{' '} + + Invite them to the organisation first + + } + /> + +
+ + )}
)} /> - +