fix(export-ui): indicate the selected format and default to xlsx

Radio.Card's only checked cue is a border tint, which is invisible at the
p="xs" the three format cards use — so the dialog gave no sign of which
format was picked. ReportExportButton's cards already carry a
Radio.Indicator; this adds the same one, dropped when the card was
compressed to an icon-over-label stack.

Default is now xlsx rather than csv: typed number and date columns mean a
spreadsheet opens it without the "is this text?" pass. It falls back to
dataset.formats[0] so the dialog can never preset a format the dataset
does not offer.

That default also made the over-cap alert misleading. It suggested
switching to CSV whenever the format was not CSV, but CSV_ROW_CAP and
XLSX_ROW_CAP are both 50_000 — from the new default that buys nothing.
It is now gated on dataset.caps.csv > cap, so it appears only from PDF,
where the cap really is lower.
This commit is contained in:
Nathnael
2026-08-20 11:23:55 +00:00
parent ac585fbbd2
commit 8949325a9c

View File

@@ -67,7 +67,12 @@ export function ExportDialog({ opened, onClose, dataset, params }: ExportDialogP
); );
const [selected, setSelected] = useState<string[]>(defaultKeys); const [selected, setSelected] = useState<string[]>(defaultKeys);
const [format, setFormat] = useState<ExportFormat>("csv"); // xlsx by default: typed number and date columns, so a spreadsheet opens it
// without the "is this text?" pass CSV needs. Falls back to whatever the
// dataset does offer rather than presetting a format it would reject.
const [format, setFormat] = useState<ExportFormat>(
() => (dataset.formats.includes("xlsx") ? "xlsx" : dataset.formats[0]),
);
const [scope, setScope] = useState("all"); const [scope, setScope] = useState("all");
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [exporting, setExporting] = useState(false); const [exporting, setExporting] = useState(false);
@@ -343,7 +348,15 @@ export function ExportDialog({ opened, onClose, dataset, params }: ExportDialogP
return ( return (
<Radio.Card key={f} value={f} radius="md" p="xs"> <Radio.Card key={f} value={f} radius="md" p="xs">
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<Icon size={18} /> {/* Radio.Card's own checked state is a border tint
and nothing else, which reads as unselected at
this size. The Indicator is what actually says
which format is picked, as the report export
dialog's cards already do. */}
<Group gap={6} wrap="nowrap">
<Radio.Indicator size="xs" />
<Icon size={18} />
</Group>
<Text size="xs" fw={500}> <Text size="xs" fw={500}>
{label} {label}
</Text> </Text>
@@ -385,7 +398,7 @@ export function ExportDialog({ opened, onClose, dataset, params }: ExportDialogP
<Text size="xs"> <Text size="xs">
{total?.toLocaleString()} rows exceeds the {cap.toLocaleString()}-row{" "} {total?.toLocaleString()} rows exceeds the {cap.toLocaleString()}-row{" "}
{FORMAT_META[format].label} limit. Narrow the filters {FORMAT_META[format].label} limit. Narrow the filters
{format !== "csv" ? ", switch to CSV," : ""} or{" "} {dataset.caps.csv > cap ? ", switch to CSV," : ""} or{" "}
<Anchor size="xs" onClick={() => setScope(String(cap))}> <Anchor size="xs" onClick={() => setScope(String(cap))}>
export the first {cap.toLocaleString()} export the first {cap.toLocaleString()}
</Anchor> </Anchor>