import { useEffect, useRef, useState } from "react"; import { Button, IconButton } from "./Button.js"; import { Icon } from "./Icon.js"; import { Select, type SelectOption } from "./Select.js"; import { TextField } from "./Field.js"; import { Window, WindowFooterActions } from "./Window.js"; export interface ShareAccessMember { id: string; name: string; email: string; role: Role; roleLabel?: string; avatarUrl?: string; immutable?: boolean; } export interface ShareAccessModalProps { open: boolean; resourceName: string; members: readonly ShareAccessMember[]; roleOptions: Array>; inviteEmail: string; inviteRole: Role; canManage?: boolean; pending?: boolean; message?: string; messageTone?: "neutral" | "success" | "danger" | "accent"; onInviteEmailChange: (email: string) => void; onInviteRoleChange: (role: Role) => void; onInvite: () => void | Promise; onMemberRoleChange?: (member: ShareAccessMember, role: Role) => void; onMemberRemove?: (member: ShareAccessMember) => void; onClose: () => void; } function initials(name: string, email: string) { const words = (name || email).split(/[\s._@-]+/).filter(Boolean); return `${words[0]?.[0] ?? "N"}${words[1]?.[0] ?? words[0]?.[1] ?? ""}`.toUpperCase(); } export function ShareAccessModal({ open, resourceName, members, roleOptions, inviteEmail, inviteRole, canManage = true, pending = false, message, messageTone = "neutral", onInviteEmailChange, onInviteRoleChange, onInvite, onMemberRoleChange, onMemberRemove, onClose, }: ShareAccessModalProps) { const [membersOpen, setMembersOpen] = useState(false); const membersRootRef = useRef(null); useEffect(() => { if (!open) setMembersOpen(false); }, [open]); useEffect(() => { if (!membersOpen) return; const handlePointerDown = (event: PointerEvent) => { if (!(event.target instanceof Node)) return; if (event.target instanceof Element && event.target.closest(".nodedc-dropdown-surface")) return; if (!membersRootRef.current?.contains(event.target)) setMembersOpen(false); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key !== "Escape") return; // An open nested dropdown owns the first Escape press. Let it close and // restore focus without collapsing the surrounding members disclosure. if (document.querySelector(".nodedc-dropdown-surface")) return; event.preventDefault(); event.stopImmediatePropagation(); setMembersOpen(false); }; document.addEventListener("pointerdown", handlePointerDown, true); document.addEventListener("keydown", handleKeyDown, true); return () => { document.removeEventListener("pointerdown", handlePointerDown, true); document.removeEventListener("keydown", handleKeyDown, true); }; }, [membersOpen]); return ( {canManage ? ( ) : null} } >
Уже добавлены {members.length ? ( <> ) :
Пока никому не открыт доступ
} {membersOpen ? (
{members.map((member) => (
{member.name || member.email} {member.name && member.name !== member.email ? {member.email} : null} {canManage && !member.immutable && onMemberRoleChange ? ( onInviteRoleChange(role)} />
) : (
Вы не можете пригласить пользователя Попросите владельца изменить ваш уровень доступа.
)} {message ?
{message}
: null}
); } export interface ShareLinkModalProps { open: boolean; title?: string; link: string; hint?: string; copyLabel?: string; onCopy?: (link: string) => void | Promise; onClose: () => void; } export function ShareLinkModal({ open, title = "Ссылка на модель", link, hint = "Ссылка открывает текущую модель и сохранённую точку обзора.", copyLabel = "Скопировать", onCopy, onClose, }: ShareLinkModalProps) { const [copied, setCopied] = useState(false); useEffect(() => { if (!open) setCopied(false); }, [open]); const copy = async () => { if (onCopy) await onCopy(link); else await navigator.clipboard.writeText(link); setCopied(true); }; return ( } > ); }