import { writeClipboardText } from "@agent-native/core/client/clipboard"; import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconCheck, IconCopy, IconLock, IconSend2, IconTrash, IconUsersGroup, IconWorld, } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; // --------------------------------------------------------------------------- // Shared types + constants // --------------------------------------------------------------------------- export type Visibility = "private" | "org" | "public"; export type Role = "viewer" | "editor" | "admin"; export interface Share { id: string; principalType: "user" | "org"; principalId: string; role: Role; } export interface SharesResponse { ownerEmail: string | null; orgId: string | null; visibility: Visibility | null; role?: "owner" | Role; shares: Share[]; } export type SharesQuery = ReturnType>; export const VIS_META: Record = { private: { Icon: IconLock, }, org: { Icon: IconUsersGroup, }, public: { Icon: IconWorld, }, }; export const ROLE_OPTIONS: Array<{ value: Role; label: string }> = [ { value: "viewer", label: "Viewer" }, { value: "editor", label: "Editor" }, { value: "admin", label: "Admin" }, ]; export function copyToClipboard(value: string): void { void writeClipboardText(value); } // --------------------------------------------------------------------------- // Optimistic visibility mutation (resource-agnostic) // --------------------------------------------------------------------------- export function useResourceVisibilityMutation( resourceType: string, resourceId: string, sharesQuery: SharesQuery, ) { const queryClient = useQueryClient(); const setVisibility = useActionMutation("set-resource-visibility"); const shareQueryKey = useMemo( () => ["action", "list-resource-shares", { resourceType, resourceId }] as const, [resourceType, resourceId], ); const setResourceVisibility = ( next: Visibility, options?: { onSuccess?: () => void }, ) => { const previous = queryClient.getQueryData(shareQueryKey); queryClient.setQueryData(shareQueryKey, (current) => current ? { ...current, visibility: next } : current, ); setVisibility.mutate( { resourceType, resourceId, visibility: next, } as any, { onSuccess: () => { void sharesQuery.refetch().finally(() => options?.onSuccess?.()); }, onError: () => { if (previous) { queryClient.setQueryData(shareQueryKey, previous); } else { queryClient.invalidateQueries({ queryKey: shareQueryKey }); } }, }, ); }; return { setResourceVisibility, isPending: setVisibility.isPending }; } // --------------------------------------------------------------------------- // Header (title + owner) // --------------------------------------------------------------------------- export function ShareCardHeader({ title, ownerEmail, reserveCloseButton = false, }: { title: string; ownerEmail?: string | null; reserveCloseButton?: boolean; }) { const t = useT(); return (
{title}
{ownerEmail ? (
{t("shareUi.owner", { email: ownerEmail })}
) : null}
); } // --------------------------------------------------------------------------- // General-access (visibility) selector // --------------------------------------------------------------------------- export function GeneralAccessSelect({ visibility, canManage, isPending, onChange, publicDescription, }: { visibility: Visibility; canManage: boolean; isPending: boolean; onChange: (next: Visibility) => void; /** Override for the "public" visibility description (e.g. Clips comment hint). */ publicDescription?: string; }) { const t = useT(); const meta = VIS_META[visibility]; const description = visibility === "public" && publicDescription ? publicDescription : t(`shareUi.visibility.${visibility}.description`); return (
{t("shareUi.generalAccess")}
{description}
); } // --------------------------------------------------------------------------- // "Make public and copy" card (shown for private/org links the user manages) // --------------------------------------------------------------------------- export function MakePublicCard({ isPending, onMakePublic, }: { isPending: boolean; onMakePublic: () => void; }) { const t = useT(); return (

{t("shareUi.restrictedLinkDescription")}

); } // --------------------------------------------------------------------------- // Copy-to-clipboard field // --------------------------------------------------------------------------- export function CopyField({ label, value, disabled, }: { label: string; value: string; disabled?: boolean; }) { const t = useT(); const [copied, setCopied] = useState(false); const copy = () => { if (disabled) return; copyToClipboard(value); setCopied(true); setTimeout(() => setCopied(false), 1400); }; return (
{label ? (
{label}
) : null}
); } // --------------------------------------------------------------------------- // Avatar chip // --------------------------------------------------------------------------- export function Avatar({ label, org }: { label: string; org?: boolean }) { return ( {org ? ( ) : ( (label.split("@")[0]?.[0] ?? label[0] ?? "?").toUpperCase() )} ); } // --------------------------------------------------------------------------- // Invite tab — invite-by-email + shares list // --------------------------------------------------------------------------- export function SharePeopleTab({ resourceType, resourceId, resourceUrl, sharesQuery, canManage, onError, }: { resourceType: string; resourceId: string; /** Optional notification deep-link passed to `share-resource`. */ resourceUrl?: string; sharesQuery: SharesQuery; canManage: boolean; onError?: (err: unknown, action: "invite" | "remove") => void; }) { const t = useT(); const share = useActionMutation("share-resource"); const unshare = useActionMutation("unshare-resource"); const [email, setEmail] = useState(""); const [role, setRole] = useState("viewer"); const [notifyPeople, setNotifyPeople] = useState(true); const hasInviteEmail = email.trim().length > 0; const data = sharesQuery.data; const shares = data?.shares ?? []; const handleAdd = () => { const trimmed = email.trim().toLowerCase(); if (!trimmed) return; share.mutate( { resourceType, resourceId, principalType: "user", principalId: trimmed, role, notify: notifyPeople, ...(resourceUrl ? { resourceUrl } : {}), } as any, { onSuccess: () => { setEmail(""); sharesQuery.refetch(); }, onError: (err: unknown) => onError?.(err, "invite"), }, ); }; const handleRemove = (s: Share) => { unshare.mutate( { resourceType, resourceId, principalType: s.principalType, principalId: s.principalId, } as any, { onSuccess: () => sharesQuery.refetch(), onError: (err: unknown) => onError?.(err, "remove"), }, ); }; return (
{canManage ? (
setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); }} autoComplete="off" className="flex-1 h-9" />
{hasInviteEmail ? ( ) : null}
) : null}
{t("shareUi.peopleWithAccess")}
    {data?.ownerEmail ? (
  • {data.ownerEmail} {t("shareUi.ownerRole")}
  • ) : null} {shares.map((s) => (
  • {s.principalId} {t(`shareUi.roles.${s.role}`)} {canManage ? ( ) : null}
  • ))} {!shares.length && !data?.ownerEmail ? (
  • {t("shareUi.noAccessYet")}
  • ) : null}
); }