import { useMemo, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { CreatableSelect } from "@app/components/v2/CreatableSelect"; import { useDebounce } from "@app/hooks"; import { useAdminGetUsers, useServerAdminCreateOrganization } from "@app/hooks/api"; import { User } from "@app/hooks/api/users/types"; import { GenericResourceNameSchema } from "@app/lib/schemas"; type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; }; type ContentProps = { onClose: () => void; }; type Invitee = Pick; type NewOption = { label: string; value: string }; const getUserLabel = (user: Invitee | NewOption) => { if (Object.prototype.hasOwnProperty.call(user, "value")) { return (user as NewOption).label; } const { firstName, lastName, username, email } = user as Invitee; const name = `${firstName ?? ""} ${lastName ?? ""}`.trim(); const userEmail = email || username; if (!name) return userEmail; return `${name}${userEmail ? ` (${userEmail})` : ""}`; }; const AddOrgSchema = z.object({ name: GenericResourceNameSchema.nonempty("Organization name required"), invitees: z .object({ id: z.string(), firstName: z.string().nullish(), lastName: z.string().nullish(), email: z.string().nullish(), username: z.string().nullish() }) .array() .min(1, "At least one admin is required") }); type FormData = z.infer; const Content = ({ onClose }: ContentProps) => { const createOrg = useServerAdminCreateOrganization(); const { handleSubmit, control, formState: { isSubmitting } } = useForm({ defaultValues: { name: "", invitees: [] }, resolver: zodResolver(AddOrgSchema) }); const [searchUserFilter, setSearchUserFilter] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500); const { data, isFetching } = useAdminGetUsers( { limit: 20, searchTerm: debouncedSearchTerm, adminsOnly: false }, { placeholderData: (prev) => prev } ); const users = useMemo(() => data?.pages.flat() ?? [], [data]); const onSubmit = async ({ name, invitees }: FormData) => { try { await createOrg.mutateAsync({ name, inviteAdminEmails: invitees .filter((user) => Boolean(user.email)) .map((user) => user.email) as string[] }); createNotification({ type: "success", text: "Successfully created organization" }); onClose(); } catch { createNotification({ text: "Failed to create organization", type: "error" }); } }; const { append } = useFieldArray({ control, name: "invitees" }); return (
( )} control={control} name="name" /> ( (

Invite new users to this organization by typing out their email address.

)} onCreateOption={(inputValue) => append({ id: `${inputValue}_${Math.random()}`, email: inputValue }) } formatCreateLabel={(inputValue) => `Invite "${inputValue}"`} isValidNewOption={(input) => Boolean(input) && z.string().email().safeParse(input).success && !users ?.flatMap((user) => { const emails: string[] = []; if (user.email) { emails.push(user.email); } if (user.username) { emails.push(user.username); } return emails; }) .includes(input) } isLoading={searchUserFilter !== debouncedSearchTerm || isFetching} className="w-full" placeholder="Search users or invite new ones..." isMulti name="members" options={users} getOptionLabel={(user) => getUserLabel(user)} getOptionValue={(user) => user.id} value={field.value} onChange={field.onChange} onInputChange={(value) => { setSearchUserFilter(value); if (!value) setDebouncedSearchTerm(""); }} />
)} control={control} name="invitees" />
); }; export const AddOrganizationModal = ({ isOpen, onOpenChange }: Props) => { return ( onOpenChange(false)} /> ); };