import { useCallback, useState, type ComponentType } from 'react' import { CircularProgress, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, type SvgIconProps, } from '@mui/material' import { FileDownload as DownloadIcon } from '@mui/icons-material' import { Tooltip } from '../../../components' import { triggerLinkDownload } from './exports' import type { DownloadItem } from './types' import { DEFAULT_DOWNLOAD_LABELS, type DownloadLabels } from './labels' export interface DownloadProps { items: readonly DownloadItem[] labels?: Partial icon?: ComponentType iconProps?: SvgIconProps /** * Fires when an item's `resolve()` rejects (or any step in the click-through * path throws). The error is normalised to a real `Error` first. Surface * to your telemetry / toast layer here; the trigger already shows a visual * error indicator independently. */ onError?: (err: Error) => void } export function Download({ items, labels, icon: Icon = DownloadIcon, iconProps, onError, }: DownloadProps) { const _labels = { ...DEFAULT_DOWNLOAD_LABELS, ...labels } const [anchorEl, setAnchorEl] = useState(null) const [isDownloading, setIsDownloading] = useState(false) const [error, setError] = useState(null) const open = useCallback((e: React.MouseEvent) => { // Clear any prior error when the user re-engages the menu — the next // attempt is a fresh interaction, not a continuation of the failed one. setError(null) setAnchorEl(e.currentTarget) }, []) const close = useCallback(() => setAnchorEl(null), []) const onSelect = useCallback( (item: DownloadItem) => { // Single-flight: ignore re-entry while a download is already in flight. // The trigger is `disabled` and the menu is closed during that window, // so this guard mostly defends against future call sites that bypass // the menu (e.g. keyboard shortcuts). if (item.disabled || isDownloading) return close() setIsDownloading(true) setError(null) item .resolve() .then(({ url, filename, revoke }) => { // `item.filename` overrides what `resolve()` returns — lets consumers // template the filename with widget metadata declaratively. triggerLinkDownload({ url, filename: item.filename ?? filename }) // Defer revoke past the current task so Safari / Firefox-on-slow-disk // can dispatch the download before the blob URL is invalidated. // Synchronous revoke here can silently cancel the download. if (revoke) setTimeout(revoke, 0) }) .catch((err: unknown) => { const e = err instanceof Error ? err : new Error(String(err)) setError(e) onError?.(e) }) .finally(() => { setIsDownloading(false) }) }, [close, isDownloading, onError], ) const triggerLabel = isDownloading ? _labels.loading : error ? _labels.error : _labels.trigger const triggerActive = Boolean(anchorEl) || isDownloading return ( <> {isDownloading ? ( ) : ( )} {items.map((item) => ( void onSelect(item)} > {item.icon && ( {item.icon} )} {item.label} ))} ) }