84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
import * as React from "react";
|
|
import { Download } from "lucide-react";
|
|
import { Button, type ButtonProps } from "@/components/ui/button";
|
|
import { defaultCsvFilename, downloadCsv, toCsv, type CsvColumn } from "@/lib/csv";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export type ExportCsvButtonProps<T> = {
|
|
/** Rows to serialize. Empty rows → button disabled with a tooltip. */
|
|
rows: T[];
|
|
/** Column descriptors (header + per-row extractor). */
|
|
columns: CsvColumn<T>[];
|
|
/** Override the download filename. Defaults to `{prefix}-{YYYY-MM-DD}.csv`. */
|
|
filename?: string;
|
|
/** Filename prefix when no explicit filename is provided. */
|
|
filenamePrefix?: string;
|
|
/** Visible label. Defaults to "Export CSV". */
|
|
label?: string;
|
|
/** Render an icon-only button with `aria-label="Export as CSV"`. */
|
|
iconOnly?: boolean;
|
|
/** Forwarded to the underlying Button. */
|
|
disabled?: boolean;
|
|
/** Forwarded to the underlying Button for layout tweaks. */
|
|
className?: string;
|
|
} & Omit<ButtonProps, "onClick" | "children" | "disabled">;
|
|
|
|
/**
|
|
* One-line CSV export: a page passes `rows` + `columns`, we serialize and
|
|
* trigger a download. Disabled (with a tooltip) when there's nothing to
|
|
* export. The "Exporting..." state is decorative — `downloadCsv` is sync —
|
|
* but it gives the click tactile feedback so the action feels deliberate.
|
|
*/
|
|
export function ExportCsvButton<T>({
|
|
rows,
|
|
columns,
|
|
filename,
|
|
filenamePrefix = "export",
|
|
label = "Export CSV",
|
|
iconOnly = false,
|
|
disabled,
|
|
className,
|
|
variant = "outline",
|
|
size,
|
|
type = "button",
|
|
...buttonProps
|
|
}: ExportCsvButtonProps<T>) {
|
|
const [busy, setBusy] = React.useState(false);
|
|
const nothingToExport = rows.length === 0;
|
|
const isDisabled = disabled || nothingToExport;
|
|
|
|
const handleClick = React.useCallback(() => {
|
|
if (isDisabled) return;
|
|
const csv = toCsv(rows, columns);
|
|
const finalName = filename ?? defaultCsvFilename(filenamePrefix);
|
|
setBusy(true);
|
|
try {
|
|
downloadCsv(finalName, csv);
|
|
} finally {
|
|
// Reset on the next tick so the user sees the busy state flash.
|
|
queueMicrotask(() => setBusy(false));
|
|
}
|
|
}, [columns, filename, filenamePrefix, isDisabled, rows]);
|
|
|
|
const tooltip = nothingToExport ? "No rows to export" : undefined;
|
|
|
|
return (
|
|
<Button
|
|
variant={variant}
|
|
size={size ?? (iconOnly ? "icon" : "sm")}
|
|
type={type}
|
|
disabled={isDisabled || busy}
|
|
onClick={handleClick}
|
|
title={tooltip}
|
|
aria-label={iconOnly ? "Export as CSV" : undefined}
|
|
data-busy={busy || undefined}
|
|
className={cn(busy && "cursor-progress", className)}
|
|
{...buttonProps}
|
|
>
|
|
<Download className="h-3.5 w-3.5" strokeWidth={1.75} aria-hidden />
|
|
{iconOnly ? null : (
|
|
<span>{busy ? "Exporting…" : label}</span>
|
|
)}
|
|
</Button>
|
|
);
|
|
} |