/** * Component: Blocked Releases Chip (request-detail surface) * Documentation: documentation/admin-features/release-blocklist.md * * Visible chip on a request row showing "N releases blocked" — click to expand * a popover listing names + reasons. Real {isOpen && position && typeof window !== 'undefined' && createPortal(

Blocked for this request

{isLoading && (

Loading…

)} {error && (

Failed to load blocked releases.

)} {data && data.entries.length === 0 && (

No blocked releases.

)} {data && data.entries.length > 0 && ( )}
, document.body )} ); } function BlockedEntryItem({ entry, onRemoved, }: { entry: BlockedReleaseRow; onRemoved: () => void; }) { const toast = useToast(); const [isUnblocking, setIsUnblocking] = useState(false); const handleUnblock = async () => { setIsUnblocking(true); try { const response = await fetchWithAuth(`/api/admin/blocklist/${entry.id}`, { method: 'DELETE', }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error || body.message || 'Failed to unblock'); } toast.success(`Unblocked: ${entry.releaseName}`); onRemoved(); } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to unblock'); } finally { setIsUnblocking(false); } }; const sourceLabel = SOURCE_BADGE_LABEL[entry.source] ?? entry.source; return (
  • {entry.releaseName}

    {sourceLabel} {entry.reason}
  • ); }