UX Standards — 08B: Export Dialog
Governs: The Export Data dialog triggered by the Download (⬇) toolbar button.
Parent rules: See 08-UDT-TOOLBAR.md and 11-MODALS-AND-DIALOGS.md first.
Overview
The Export dialog lets users configure precisely what data to download before the export executes. It is a blocking Dialog (not floating) that must be confirmed or cancelled before interacting with the table again.
It is triggered by clicking the Download icon in the toolbar, then selecting a format from the dropdown.
Trigger Flow
1. User clicks [⬇ Download] button in toolbar
2. Export format dropdown appears:
┌─────────────────┐
│ CSV │
│ TSV │
│ Excel │
│ ─────────────── │
│ JSON │
│ XML │
│ ─────────────── │
│ PDF │
└─────────────────┘
3. User selects a format (e.g., Excel)
4. Export Data dialog opens
5. User configures records + columns
6. User clicks Export
7. File downloads
Visual Anatomy
┌─────────────────────────────────────────────────────┐
│ Export Data [✕] │
│ Choose which records and columns to include. │
├─────────────────────────────────────────────────────┤
│ │
│ Records │
│ ● All records (14) │
│ ○ Selected records only (0) ← disabled when 0 │
│ │
│ Columns │
│ ● Visible columns only │
│ ○ All columns │
│ │
├─────────────────────────────────────────────────────┤
│ [Cancel] [Export] │
└─────────────────────────────────────────────────────┘
Component Spec
<Dialog open={exportDialogOpen} onOpenChange={setExportDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Export Data</DialogTitle>
<DialogDescription>
Choose which records and columns to include in the export.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
{/* Records section */}
<div className="space-y-2">
<Label className="text-sm font-medium">Records</Label>
<RadioGroup
value={exportSelectedOnly ? 'selected' : 'all'}
onValueChange={(v) => setExportSelectedOnly(v === 'selected')}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="all" id="export-all" />
<Label htmlFor="export-all" className="font-normal">
All records ({totalRowCount})
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="selected"
id="export-selected"
disabled={!hasSelection} // Disabled when 0 rows selected
/>
<Label
htmlFor="export-selected"
className={cn('font-normal', !hasSelection && 'text-muted-foreground')}
>
Selected records only ({selectedCount})
</Label>
</div>
</RadioGroup>
</div>
{/* Columns section */}
<div className="space-y-2">
<Label className="text-sm font-medium">Columns</Label>
<RadioGroup
value={exportAllColumns ? 'all' : 'visible'}
onValueChange={(v) => setExportAllColumns(v === 'all')}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="visible" id="export-visible-cols" />
<Label htmlFor="export-visible-cols" className="font-normal">
Visible columns only
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="all" id="export-all-cols" />
<Label htmlFor="export-all-cols" className="font-normal">
All columns
</Label>
</div>
</RadioGroup>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => {
setExportDialogOpen(false);
setPendingExportFormat(null);
}}
>
Cancel
</Button>
<Button onClick={handleExportConfirm}>
Export
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Records Options
| Option | Label Format | Pre-selected When | Disabled When |
|---|---|---|---|
| All records | All records (N) — N = total filtered row count | Default / no selection | Never |
| Selected records only | Selected records only (N) — N = checked row count | Any rows are checkbox-selected | 0 rows selected |
Auto-select behavior:
- If user has checkbox-selected rows before clicking Export → "Selected records only" pre-selected
- If no rows selected → "All records" pre-selected and "Selected records only" disabled
// Pre-select based on selection state when dialog opens:
const handleExportClick = (format: ExportFormat) => {
setPendingExportFormat(format);
setExportSelectedOnly(hasSelection); // auto-select based on current selection
setExportAllColumns(false); // always default to visible only
setExportDialogOpen(true);
};
Columns Options
| Option | Behavior |
|---|---|
| Visible columns only | Exports only the columns currently visible (respects user's column visibility settings) |
| All columns | Exports every column defined in the table, including hidden ones (IDs, audit fields, etc.) |
Default: Visible columns only — this is what the user sees and is almost always what they want.
"All columns" is for data analysts and ETL workflows who need the complete record.
Export Formats Reference
| Format | Extension | Best For |
|---|---|---|
| CSV | .csv | Data import, universal compatibility |
| TSV | .tsv | Tab-delimited, better for text with commas |
| Excel | .xlsx | Business users, formatted spreadsheets |
| JSON | .json | API consumers, developers |
| XML | .xml | Legacy system integration |
.pdf | Reports, printing, archiving |
Export Execution
const handleExportConfirm = () => {
if (!pendingExportFormat) return;
// Call the page's onExport handler with configuration
onExport?.(
pendingExportFormat,
exportSelectedOnly,
exportAllColumns
);
setExportDialogOpen(false);
setPendingExportFormat(null);
};
The onExport callback in DocTypeListPage / DataTableToolbar receives:
format: The selected format stringselectedOnly: Whether to export only checked rowsallColumns: Whether to include hidden columns
The implementation triggers a browser download:
// Typical onExport implementation in the page:
const handleExport = (format: ExportFormat, selectedOnly: boolean, allColumns: boolean) => {
const rows = selectedOnly
? table.getFilteredSelectedRowModel().rows.map(r => r.original)
: table.getPrePaginationRowModel().rows.map(r => r.original);
const columns = allColumns
? allColumnDefs
: table.getVisibleLeafColumns().filter(c => c.id !== 'select' && c.id !== 'actions');
const data = formatForExport(rows, columns, format);
downloadFile(data, `${doctype}-export-${Date.now()}.${format}`, getMimeType(format));
toast.success(`Exported ${rows.length} records`);
};
Feedback After Export
// Success toast — shown after download initiates
toast.success(`Exported ${rowCount} records`, {
description: `${format.toUpperCase()} file downloaded.`
});
The dialog closes immediately after clicking Export (before the download completes). If the export fails:
toast.error('Export failed', {
description: error.message
});
// Dialog stays open so user can try again
PDF Export Special Case
PDF exports are handled differently — they produce a formatted report rather than raw data:
Format: PDF
→ Opens a Print Preview in a new tab/popup window
→ User configures page layout, orientation, page size
→ User clicks Print/Save to PDF
Or alternatively, generates a server-side PDF:
Format: PDF
→ POSTs to /api/export/pdf with row data
→ Returns PDF blob
→ Browser downloads the file
State Cleanup
After the dialog closes (Cancel or Export):
// Always clean up
setExportDialogOpen(false);
setPendingExportFormat(null);
// Don't reset exportSelectedOnly or exportAllColumns — remember user's last choice
Container Sizing
The Export dialog is always sm:max-w-md (448px). It never needs to be wider — the two RadioGroups are compact.
Accessibility
| Requirement | Implementation |
|---|---|
| Dialog title | DialogTitle = "Export Data" |
| Description | DialogDescription explains what the dialog does |
| Radio inputs | RadioGroupItem with associated <Label htmlFor> |
| Disabled state | disabled attribute + text-muted-foreground label |
| Focus | Auto-focuses first radio on open |
| Escape | Closes dialog (shadcn Dialog handles this) |
Integration with DocTypeListPage
<DocTypeListPage
table={table}
onExport={(format, selectedOnly, allColumns) => {
handleExport(format, selectedOnly, allColumns);
}}
permissions={{ canExport: userCan('export', doctype) }}
...
/>
The onExport handler is only called after the user configures and confirms in the Export dialog — the toolbar Download button does NOT export immediately.
Violation Checklist
- Dialog size is
sm:max-w-md(not wider) - Title is "Export Data", description explains purpose
- Records section uses
RadioGroupwithLabelcomponents - "Selected records only" disabled when 0 rows selected
- Row count shown in parentheses for both options:
All records (14),Selected records only (3) - Columns section uses
RadioGroupwithLabelcomponents - Default pre-selection: "All records" + "Visible columns only"
- Auto-pre-select "Selected records only" when rows are checkbox-selected before opening
- Export button is primary (green/default variant)
- Cancel button is
variant="outline" - Dialog closes immediately after Export click
- Success
toast.successshown after download initiates - Error
toast.errorshown on failure — dialog stays open -
onExportreceives all three params: format, selectedOnly, allColumns