Files
mimic/frontend/src/components/ExportEngagementButton.tsx

101 lines
3.2 KiB
TypeScript
Raw Normal View History

import { useEffect, useRef, useState } from 'react';
import { ChevronDown, Download, Loader2 } from 'lucide-react';
import { downloadEngagementExport, type ExportFormat } from '@/api/exports';
import { useToast } from '@/hooks/useToast';
interface ExportEngagementButtonProps {
engagementId: number;
}
const FORMATS: { label: string; value: ExportFormat }[] = [
{ label: 'Markdown', value: 'md' },
{ label: 'CSV', value: 'csv' },
{ label: 'PDF', value: 'pdf' },
];
export function ExportEngagementButton({ engagementId }: ExportEngagementButtonProps): JSX.Element {
const { push } = useToast();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState<ExportFormat | null>(null);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onPointerDown = (e: PointerEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('pointerdown', onPointerDown);
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('pointerdown', onPointerDown);
document.removeEventListener('keydown', onKeyDown);
};
}, [open]);
const handleDownload = async (format: ExportFormat) => {
setLoading(format);
try {
await downloadEngagementExport(engagementId, format);
setOpen(false);
} catch (err) {
push(err instanceof Error ? err.message : 'Export failed', 'error');
} finally {
setLoading(null);
}
};
return (
<div className="relative" ref={ref} data-testid="export-dropdown">
<div className="inline-flex">
<button
type="button"
className="btn-outline rounded-r-none border-r-0"
onClick={() => setOpen((v) => !v)}
data-testid="export-btn"
>
<Download size={14} aria-hidden /> Export
</button>
<button
type="button"
aria-label="Export options"
aria-expanded={open}
className="btn-outline rounded-l-none px-sm"
onClick={() => setOpen((v) => !v)}
data-testid="export-dropdown-toggle"
>
<ChevronDown size={14} aria-hidden />
</button>
</div>
{open ? (
<div
className="absolute right-0 top-full mt-xxs bg-canvas border border-hairline rounded-md shadow-floating dark:shadow-floating-dark z-20 min-w-[160px]"
role="menu"
>
{FORMATS.map(({ label, value }) => (
<button
key={value}
type="button"
role="menuitem"
disabled={loading !== null}
className="w-full text-left px-md py-sm text-[14px] text-ink hover:bg-cloud dark:hover:bg-fog flex items-center gap-sm disabled:opacity-50"
onClick={() => handleDownload(value)}
data-testid={`export-format-${value}`}
>
{loading === value ? (
<Loader2 size={12} className="animate-spin" aria-hidden />
) : null}
{label}
</button>
))}
</div>
) : null}
</div>
);
}