import { create } from "@bufbuild/protobuf"; import { CheckIcon, LoaderCircleIcon, SearchIcon } from "lucide-react"; import { type FormEvent, useEffect, useState } from "react"; import toast from "react-hot-toast"; import UserAvatar from "@/components/UserAvatar"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useCreateSpaceInvitation } from "@/hooks/useSpaceQueries"; import { useUsersByUsernames } from "@/hooks/useUserQueries"; import { handleError } from "@/lib/error"; import { extractSpaceUidFromName } from "@/lib/space-display"; import { type Space, SpaceInvitationSchema, SpaceMember_Role } from "@/types/proto/api/v1/space_service_pb"; import { useTranslate } from "@/utils/i18n"; const LOOKUP_DELAY_MS = 300; interface Props { open: boolean; onOpenChange: (open: boolean) => void; space: Space; viewerName: string; memberUserNames: Set; pendingInviteeNames: Set; } const InviteSpaceMemberDialog = ({ open, onOpenChange, space, viewerName, memberUserNames, pendingInviteeNames }: Props) => { const t = useTranslate(); const spaceUid = extractSpaceUidFromName(space.name); const createInvitation = useCreateSpaceInvitation(viewerName); const [query, setQuery] = useState(""); const [lookupUsername, setLookupUsername] = useState(""); const [selectedUserName, setSelectedUserName] = useState(""); const [role, setRole] = useState(SpaceMember_Role.USER); const roleOptions = [ { value: String(SpaceMember_Role.USER), label: t("setting.spaces.space-user") }, { value: String(SpaceMember_Role.ADMIN), label: t("setting.spaces.space-admin") }, ]; const normalizedUsername = query.trim().replace(/^@/, ""); const lookupSettled = lookupUsername === normalizedUsername; const usersQuery = useUsersByUsernames(lookupUsername ? [lookupUsername] : [], { enabled: open && Boolean(lookupUsername), }); const candidate = lookupUsername ? usersQuery.data?.get(lookupUsername) : undefined; const isCurrentUser = candidate?.name === viewerName; const isMember = candidate ? memberUserNames.has(candidate.name) : false; const isPendingInvitee = candidate ? pendingInviteeNames.has(candidate.name) : false; const isCandidateAvailable = Boolean(candidate && !isCurrentUser && !isMember && !isPendingInvitee); const isSelected = Boolean(candidate && candidate.name === selectedUserName); useEffect(() => { const timeout = window.setTimeout(() => setLookupUsername(normalizedUsername), LOOKUP_DELAY_MS); return () => window.clearTimeout(timeout); }, [normalizedUsername]); useEffect(() => { if (!open) { setQuery(""); setLookupUsername(""); setSelectedUserName(""); setRole(SpaceMember_Role.USER); } }, [open]); const handleQueryChange = (value: string) => { setQuery(value); setSelectedUserName(""); }; const handleOpenChange = (nextOpen: boolean) => { if (!nextOpen && createInvitation.isPending) { return; } onOpenChange(nextOpen); }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (!candidate || !isSelected || !isCandidateAvailable || createInvitation.isPending) { return; } try { await createInvitation.mutateAsync({ parent: space.name, spaceInvitation: create(SpaceInvitationSchema, { invitee: candidate.name, role, }), }); toast.success(t("setting.spaces.invite-success", { username: candidate.username })); onOpenChange(false); } catch (error) { handleError(error, toast.error, { context: "Create space invitation" }); } }; const resultStatus = isCurrentUser ? t("setting.spaces.invite-yourself") : isMember ? t("setting.spaces.already-member") : isPendingInvitee ? t("setting.spaces.already-invited") : t("setting.spaces.active-user"); return (
{t("setting.spaces.invite-to", { space: space.title })} {t("setting.spaces.invite-description")} {t("space.custom-id-label")}: {spaceUid}
handleQueryChange(event.target.value)} autoFocus autoComplete="off" placeholder={t("setting.spaces.search-user-placeholder")} className="pl-8" /> {normalizedUsername && (!lookupSettled || usersQuery.isFetching) ? ( ) : null}
{candidate && lookupSettled ? ( ) : null} {normalizedUsername && lookupSettled && usersQuery.isSuccess && !candidate ? (

{t("setting.spaces.user-not-found", { username: normalizedUsername })}

) : null}

{role === SpaceMember_Role.ADMIN ? t("setting.spaces.admin-role-description") : t("setting.spaces.user-role-description")}

); }; export default InviteSpaceMemberDialog;