diff --git a/library/composables/useCsvExport.js b/library/composables/useCsvExport.js index 018e175ad..926bcfdd0 100644 --- a/library/composables/useCsvExport.js +++ b/library/composables/useCsvExport.js @@ -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})` diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue index 17a074dfc..9b8ee72ce 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue @@ -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)