Skip to main content

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

OptionLabel FormatPre-selected WhenDisabled When
All recordsAll records (N) — N = total filtered row countDefault / no selectionNever
Selected records onlySelected records only (N) — N = checked row countAny rows are checkbox-selected0 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

OptionBehavior
Visible columns onlyExports only the columns currently visible (respects user's column visibility settings)
All columnsExports 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

FormatExtensionBest For
CSV.csvData import, universal compatibility
TSV.tsvTab-delimited, better for text with commas
Excel.xlsxBusiness users, formatted spreadsheets
JSON.jsonAPI consumers, developers
XML.xmlLegacy system integration
PDF.pdfReports, 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 string
  • selectedOnly: Whether to export only checked rows
  • allColumns: 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

RequirementImplementation
Dialog titleDialogTitle = "Export Data"
DescriptionDialogDescription explains what the dialog does
Radio inputsRadioGroupItem with associated <Label htmlFor>
Disabled statedisabled attribute + text-muted-foreground label
FocusAuto-focuses first radio on open
EscapeCloses 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 RadioGroup with Label components
  • "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 RadioGroup with Label components
  • 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.success shown after download initiates
  • Error toast.error shown on failure — dialog stays open
  • onExport receives all three params: format, selectedOnly, allColumns