A CSV export honours the typed search, so the file matches the table on screen

With no facet filter and no selection, a table narrowed by typing exported every
row in the source. The export now takes an optional search transform, and the
data table passes the matcher it already uses for its visible rows. An explicit
selection still outranks the search, and a caller that passes nothing is
unchanged.
This commit is contained in:
Doxa Maps 2026-09-27 18:52:46 +00:00
parent 9d7d630fd6
commit 34573a27c9
2 changed files with 13 additions and 3 deletions

View file

@ -74,6 +74,12 @@ export const EXPORT_FIELDS = [
* It is applied to the COMMENT LINE ONLY — never to a data column (see buildCsv).
*/
export function useCsvExport({ rowStore, rows, selectionSet, filterSet, FACET_ACC, dataOpts = {}, labelFor = null,
// ★ THE TYPED SEARCH NARROWS THE EXPORT TOO. Optional rows->rows transform, so a caller that does not
// pass it behaves exactly as before. Without it the export ignored the search box completely: with no
// facet filter and no selection, pickRows() fell through to EVERY row, so a reader who typed "Bhutan"
// and saw 13 rows downloaded all 2,107. Not applied to an explicit selection — picking rows by hand
// outranks a text filter, which is the documented priority model, and the reader can see what they picked.
searchFilter = null,
// What this export calls itself, and who to credit for the data. Defaults keep every existing
// caller's file identical to the byte.
exportLabel = 'Research Dashboard', sources = '',
@ -160,6 +166,7 @@ export function useCsvExport({ rowStore, rows, selectionSet, filterSet, FACET_AC
}
function pickRows() {
const narrow = (list) => (typeof searchFilter === 'function' ? searchFilter(list) : list)
if (selectionSet && selectionSet.size) {
return [...selectionSet].map((s) => rowStore.value.get(s)).filter(Boolean)
}
@ -168,9 +175,9 @@ export function useCsvExport({ rowStore, rows, selectionSet, filterSet, FACET_AC
// The bus's own predicate, handed in by the shell — the exporter must select the same rows
// the reader is looking at, and a private copy of the rule is how an export starts
// disagreeing with the screen it was taken from.
return (rows.value || []).filter((r) => rowMatches(r, FACET_ACC))
return narrow((rows.value || []).filter((r) => rowMatches(r, FACET_ACC)))
}
return rows.value || []
return narrow(rows.value || [])
}
function scopeLabel() {
if (selectionSet && selectionSet.size) return `selection (${selectionSet.size})`

View file

@ -547,7 +547,10 @@ const rowCount = computed(() => filtered.value.length)
// columns are untouched.
const { labelFor: csvLabelFor } = useTreeLegendLabels(cfg, inject('dataStore', null), inject('searchSourceId', null))
const csv = dash && bus
? useCsvExport({ rowStore: dash.rowStore, rows: dash.rows, selectionSet: bus.selectionSet, filterSet: bus.filterSet, FACET_ACC: FACETS, dataOpts: cfg.data, labelFor: csvLabelFor })
? useCsvExport({ rowStore: dash.rowStore, rows: dash.rows, selectionSet: bus.selectionSet, filterSet: bus.filterSet, FACET_ACC: FACETS, dataOpts: cfg.data, labelFor: csvLabelFor,
// The table already narrows the screen with this exact matcher (see visibleRows); handing it to the
// exporter is what makes the download agree with what the reader is looking at.
searchFilter: (list) => filterRowsBySearch(list, matchSearch) })
: null
const exportToast = ref('')
const exporting = ref(false)