From 6845f866897a3ce8efc86f85888b66776b6928fe Mon Sep 17 00:00:00 2001
From: Andrew Kingston <andrew@kingston.dev>
Date: Mon, 20 Jun 2022 12:32:13 +0100
Subject: [PATCH] Add option to customise which columns are exported in export
 data action

---
 .../actions/ExportData.svelte                 | 63 +++++++++++++------
 packages/client/src/utils/buttonActions.js    |  1 +
 packages/frontend-core/src/api/rows.js        |  5 +-
 .../src/api/controllers/row/external.js       | 21 ++++++-
 4 files changed, 66 insertions(+), 24 deletions(-)

diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/ExportData.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/ExportData.svelte
index 062b9abd4c..aa3bf2a36b 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/ExportData.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/ExportData.svelte
@@ -1,27 +1,18 @@
 <script>
-  import { Label, Select, Body } from "@budibase/bbui"
-  import { findAllMatchingComponents } from "builderStore/componentUtils"
+  import { Label, Select, Body, Multiselect } from "@budibase/bbui"
+  import {
+    findAllMatchingComponents,
+    findComponent,
+  } from "builderStore/componentUtils"
   import { currentAsset } from "builderStore"
   import { onMount } from "svelte"
+  import {
+    getDatasourceForProvider,
+    getSchemaForDatasource,
+  } from "builderStore/dataBinding"
 
   export let parameters
 
-  $: tables = findAllMatchingComponents($currentAsset?.props, component =>
-    component._component.endsWith("table")
-  ).map(table => ({
-    label: table._instanceName,
-    value: table._id,
-  }))
-
-  $: tableBlocks = findAllMatchingComponents($currentAsset?.props, component =>
-    component._component.endsWith("tableblock")
-  ).map(block => ({
-    label: block._instanceName,
-    value: `${block._id}-table`,
-  }))
-
-  $: componentOptions = tables.concat(tableBlocks)
-
   const FORMATS = [
     {
       label: "CSV",
@@ -33,6 +24,32 @@
     },
   ]
 
+  $: tables = findAllMatchingComponents($currentAsset?.props, component =>
+    component._component.endsWith("table")
+  ).map(table => ({
+    label: table._instanceName,
+    value: table._id,
+  }))
+  $: tableBlocks = findAllMatchingComponents($currentAsset?.props, component =>
+    component._component.endsWith("tableblock")
+  ).map(block => ({
+    label: block._instanceName,
+    value: `${block._id}-table`,
+  }))
+  $: componentOptions = tables.concat(tableBlocks)
+  $: columnOptions = getColumnOptions(parameters.tableComponentId)
+
+  const getColumnOptions = tableId => {
+    // Strip block suffix if block component
+    if (tableId?.includes("-")) {
+      tableId = tableId.split("-")[0]
+    }
+    const selectedTable = findComponent($currentAsset?.props, tableId)
+    const datasource = getDatasourceForProvider($currentAsset, selectedTable)
+    const { schema } = getSchemaForDatasource($currentAsset, datasource)
+    return Object.keys(schema || {})
+  }
+
   onMount(() => {
     if (!parameters.type) {
       parameters.type = "csv"
@@ -53,10 +70,16 @@
     <Select
       bind:value={parameters.tableComponentId}
       options={componentOptions}
+      on:change={() => (parameters.columns = [])}
     />
-
     <Label small>Export as</Label>
     <Select bind:value={parameters.type} options={FORMATS} />
+    <Label small>Export columns</Label>
+    <Multiselect
+      placeholder="All columns"
+      bind:value={parameters.columns}
+      options={columnOptions}
+    />
   </div>
 </div>
 
@@ -80,7 +103,7 @@
     display: grid;
     column-gap: var(--spacing-xs);
     row-gap: var(--spacing-s);
-    grid-template-columns: 70px 1fr;
+    grid-template-columns: 90px 1fr;
     align-items: center;
   }
 </style>
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 473b563bb1..319c0a5feb 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -270,6 +270,7 @@ const exportDataHandler = async action => {
         tableId: selection.tableId,
         rows: selection.selectedRows,
         format: action.parameters.type,
+        columns: action.parameters.columns,
       })
       download(data, `${selection.tableId}.${action.parameters.type}`)
     } catch (error) {
diff --git a/packages/frontend-core/src/api/rows.js b/packages/frontend-core/src/api/rows.js
index 9f980678c5..70030d7f80 100644
--- a/packages/frontend-core/src/api/rows.js
+++ b/packages/frontend-core/src/api/rows.js
@@ -65,12 +65,15 @@ export const buildRowEndpoints = API => ({
    * Exports rows.
    * @param tableId the table ID to export the rows from
    * @param rows the array of rows to export
+   * @param format the format to export (csv or json)
+   * @param columns which columns to export (all if undefined)
    */
-  exportRows: async ({ tableId, rows, format }) => {
+  exportRows: async ({ tableId, rows, format, columns }) => {
     return await API.post({
       url: `/api/${tableId}/rows/exportRows?format=${format}`,
       body: {
         rows,
+        columns,
       },
       parseResponse: async response => {
         return await response.text()
diff --git a/packages/server/src/api/controllers/row/external.js b/packages/server/src/api/controllers/row/external.js
index 29238f2678..b1c322b8b6 100644
--- a/packages/server/src/api/controllers/row/external.js
+++ b/packages/server/src/api/controllers/row/external.js
@@ -157,7 +157,8 @@ exports.validate = async () => {
 exports.exportRows = async ctx => {
   const { datasourceId } = breakExternalTableId(ctx.params.tableId)
   const db = getAppDB()
-  let format = ctx.query.format
+  const format = ctx.query.format
+  const { columns } = ctx.request.body
   const datasource = await db.get(datasourceId)
   if (!datasource || !datasource.entities) {
     ctx.throw(400, "Datasource has not been configured for plus API.")
@@ -171,13 +172,27 @@ exports.exportRows = async ctx => {
   }
 
   let result = await exports.search(ctx)
-  let headers = Object.keys(result.rows[0])
+  let rows = []
+
+  // Filter data to only specified columns if required
+  if (columns && columns.length) {
+    for (let i = 0; i < result.rows.length; i++) {
+      rows[i] = {}
+      for (let column of columns) {
+        rows[i][column] = result.rows[i][column]
+      }
+    }
+  } else {
+    rows = result.rows
+  }
+
+  let headers = Object.keys(rows[0])
   const exporter = exporters[format]
   const filename = `export.${format}`
 
   // send down the file
   ctx.attachment(filename)
-  return apiFileReturn(exporter(headers, result.rows))
+  return apiFileReturn(exporter(headers, rows))
 }
 
 exports.fetchEnrichedRow = async ctx => {