import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Globe, Lock, Users } from "lucide-react"; import type { Issue, IssueVisibility } from "@paperclipai/shared"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { issuesApi } from "@/api/issues"; import { useToastActions } from "@/context/ToastContext"; import { queryKeys } from "@/lib/queryKeys"; import { cn } from "@/lib/utils"; import { IssueShareSheet, type ShareSheetImplicitPrincipal } from "./IssueShareSheet"; const MENU_ITEM_CLASS = "flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"; const NON_SETTER_TOOLTIP = "Only the task owner or an admin can change who can see this task."; /** * Privacy actions for the task `⋯` menu. Renders the menu buttons plus the * two dialogs. The render prop keeps this component outside the menu's * unmounting content, so dialogs survive the menu closing. `closeMenu` collapses the parent popover when an item that * opens a dialog is clicked. * * Setter rules (locked decision, server-gated via * `resolveManagedIssueForPrivacy`): only the responsible user + admins can * change visibility or grants. Non-setters see the items disabled with a * tooltip. Make-**public** always confirms (one-way disclosure); make-private * never does. */ export function IssuePrivacyActions({ issue, companyId, canManage, closeMenu, implicitPrincipals = [], children, }: { issue: Pick & Partial>; companyId: string; canManage: boolean; closeMenu: () => void; implicitPrincipals?: ShareSheetImplicitPrincipal[]; children: (menuItems: React.ReactNode) => React.ReactNode; }) { const queryClient = useQueryClient(); const { pushToast } = useToastActions(); const [shareOpen, setShareOpen] = useState(false); const [makePublicOpen, setMakePublicOpen] = useState(false); const isPrivate = issue.visibility === "private"; const constraintsQuery = useQuery({ queryKey: [...queryKeys.issues.privacyConstraints(issue.id), issue.privacyParentIssueId ?? null, issue.projectId ?? null], queryFn: () => issuesApi.privacyConstraints(issue.id), enabled: canManage && isPrivate, }); const publicBlockedReason = constraintsQuery.isError ? "Couldn't check task privacy. Retry before making this task public." : !constraintsQuery.data || constraintsQuery.isFetching ? "Checking task privacy…" : constraintsQuery.data.publicBlockedBy === "parent" ? "Move this task out of its private parent before making it public." : constraintsQuery.data.publicBlockedBy === "project" ? "Move this task out of its private project before making it public." : null; const visibilityMutation = useMutation({ mutationFn: (visibility: IssueVisibility) => issuesApi.setVisibility(issue.id, visibility), onSuccess: (_result, visibility) => { // Privacy can change descendants and remove a task from a private project. queryClient.invalidateQueries({ queryKey: ["issues"] }); queryClient.invalidateQueries({ queryKey: queryKeys.projects.all(companyId) }); if (issue.identifier) { queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issue.identifier) }); } queryClient.invalidateQueries({ queryKey: queryKeys.issues.accessGrants(issue.id) }); setMakePublicOpen(false); pushToast({ title: visibility === "private" ? "Task is now private" : "Task is now public", tone: "success", }); }, onError: (error) => { pushToast({ title: "Couldn't change visibility", body: (error as Error).message, tone: "error" }); }, }); function withTooltip(node: React.ReactNode, blockedReason?: string | null) { if (canManage && !blockedReason) return node; return ( {node} {canManage ? blockedReason : NON_SETTER_TOOLTIP} ); } return ( {children(isPrivate ? ( <> {withTooltip( , )} {withTooltip( , publicBlockedReason, )} {canManage && constraintsQuery.isError ? ( ) : null} ) : ( withTooltip( , ) ))} Make this task public? Everyone in the company will be able to read this task, its comments, documents, and run history. Existing private subtasks keep their privacy.{" "} {constraintsQuery.data?.leavesPersonalProject ? "This task will also leave its personal project. " : null} Content already seen by others cannot be taken back. Keep private { event.preventDefault(); visibilityMutation.mutate("open"); }} disabled={visibilityMutation.isPending || Boolean(publicBlockedReason)} > {visibilityMutation.isPending ? "Making public…" : "Make public"} ); }