mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 20:35:24 +00:00
176 lines
No EOL
7.9 KiB
TypeScript
176 lines
No EOL
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useForm } from "react-hook-form";
|
|
import { useCallback, useState } from "react";
|
|
import { z } from "zod";
|
|
import { PlusCircleIcon, Loader2 } from "lucide-react";
|
|
import { OrgRole } from "@prisma/client";
|
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
|
import { createInvites } from "@/actions";
|
|
import { useDomain } from "@/hooks/useDomain";
|
|
import { isServiceError } from "@/lib/utils";
|
|
import { useToast } from "@/components/hooks/use-toast";
|
|
import { useRouter } from "next/navigation";
|
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
|
export const inviteMemberFormSchema = z.object({
|
|
emails: z.array(z.object({
|
|
email: z.string().email()
|
|
}))
|
|
.refine((emails) => {
|
|
const emailSet = new Set(emails.map(e => e.email.toLowerCase()));
|
|
return emailSet.size === emails.length;
|
|
}, "Duplicate email addresses are not allowed")
|
|
});
|
|
|
|
interface InviteMemberCardProps {
|
|
currentUserRole: OrgRole;
|
|
}
|
|
|
|
export const InviteMemberCard = ({ currentUserRole }: InviteMemberCardProps) => {
|
|
const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const domain = useDomain();
|
|
const { toast } = useToast();
|
|
const router = useRouter();
|
|
const captureEvent = useCaptureEvent();
|
|
|
|
const form = useForm<z.infer<typeof inviteMemberFormSchema>>({
|
|
resolver: zodResolver(inviteMemberFormSchema),
|
|
defaultValues: {
|
|
emails: [{ email: "" }]
|
|
},
|
|
});
|
|
|
|
const addEmailField = useCallback(() => {
|
|
const emails = form.getValues().emails;
|
|
form.setValue('emails', [...emails, { email: "" }]);
|
|
}, [form]);
|
|
|
|
const onSubmit = useCallback((data: z.infer<typeof inviteMemberFormSchema>) => {
|
|
setIsLoading(true);
|
|
createInvites(data.emails.map(e => e.email), domain)
|
|
.then((res) => {
|
|
if (isServiceError(res)) {
|
|
toast({
|
|
description: `❌ Failed to invite members. Reason: ${res.message}`
|
|
});
|
|
captureEvent('wa_invite_member_card_invite_fail', {
|
|
error: res.errorCode,
|
|
num_emails: data.emails.length,
|
|
});
|
|
} else {
|
|
form.reset();
|
|
router.push(`?tab=invites`);
|
|
router.refresh();
|
|
toast({
|
|
description: `✅ Successfully invited ${data.emails.length} members`
|
|
});
|
|
captureEvent('wa_invite_member_card_invite_success', {
|
|
num_emails: data.emails.length,
|
|
});
|
|
}
|
|
})
|
|
.finally(() => {
|
|
setIsLoading(false);
|
|
});
|
|
}, [domain, form, toast, router, captureEvent]);
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Invite Member</CardTitle>
|
|
<CardDescription>Invite new members to your organization.</CardDescription>
|
|
</CardHeader>
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(() => setIsInviteDialogOpen(true))}>
|
|
<CardContent className="space-y-4">
|
|
<FormLabel>Email Address</FormLabel>
|
|
{form.watch('emails').map((_, index) => (
|
|
<FormField
|
|
key={index}
|
|
control={form.control}
|
|
name={`emails.${index}.email`}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
className="max-w-md"
|
|
placeholder="melissa@example.com"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
))}
|
|
{form.formState.errors.emails?.root?.message && (
|
|
<FormMessage>{form.formState.errors.emails.root.message}</FormMessage>
|
|
)}
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={addEmailField}
|
|
>
|
|
<PlusCircleIcon className="w-4 h-4 mr-0.5" />
|
|
Add more
|
|
</Button>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-end">
|
|
<Button
|
|
size="sm"
|
|
type="submit"
|
|
disabled={currentUserRole !== OrgRole.OWNER || isLoading}
|
|
>
|
|
{isLoading && <Loader2 className="w-4 h-4 mr-0.5 animate-spin" />}
|
|
Invite
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Form>
|
|
</Card>
|
|
<AlertDialog
|
|
open={isInviteDialogOpen}
|
|
onOpenChange={setIsInviteDialogOpen}
|
|
>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Invite Team Members</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{`Your team is growing! By confirming, you will be inviting ${form.getValues().emails.length} new members to your organization. Your subscription's seat count will be adjusted when a member accepts their invitation.`}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<div className="max-h-[400px] overflow-y-auto divide-y">
|
|
{form.getValues().emails.map(({ email }, index) => (
|
|
<p
|
|
key={index}
|
|
className="text-sm p-2"
|
|
>
|
|
{email}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={() => captureEvent('wa_invite_member_card_invite_cancel', {
|
|
num_emails: form.getValues().emails.length,
|
|
})}>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => onSubmit(form.getValues())}
|
|
>
|
|
Invite
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
)
|
|
} |