From 6e4c2b7242c7a67049a43eaf433543babf1a6a40 Mon Sep 17 00:00:00 2001
From: melohagan <101575380+melohagan@users.noreply.github.com>
Date: Tue, 27 Feb 2024 09:23:49 +0000
Subject: [PATCH 1/2] Export data make CSV delimiter configurable (#13028)
* Add delimiter option
* Add custom delimiter
* external export delimiter
* Custom headers for row export
* External export rows custom headers
* Support custom JSON export labels
* Handle export table source switch
* update account portal
* Add space as delimiter
* Refactor
* update account portal
---
packages/account-portal | 2 +-
.../actions/ExportData.svelte | 102 +++++++++++++-----
.../controls/ColumnEditor/ColumnEditor.svelte | 6 ++
packages/client/src/utils/buttonActions.js | 6 +-
packages/frontend-core/src/api/rows.js | 13 ++-
.../server/src/api/controllers/row/index.ts | 5 +-
.../src/api/controllers/view/exporters.ts | 18 +++-
packages/server/src/sdk/app/rows/search.ts | 2 +
.../src/sdk/app/rows/search/external.ts | 21 +++-
.../src/sdk/app/rows/search/internal.ts | 21 +++-
packages/server/src/sdk/app/rows/utils.ts | 19 +++-
packages/types/src/api/web/app/rows.ts | 2 +
12 files changed, 174 insertions(+), 43 deletions(-)
diff --git a/packages/account-portal b/packages/account-portal
index ab324e35d8..de6d44c372 160000
--- a/packages/account-portal
+++ b/packages/account-portal
@@ -1 +1 @@
-Subproject commit ab324e35d855012bd0f49caa53c6dd765223c6fa
+Subproject commit de6d44c372a7f48ca0ce8c6c0c19311d4bc21646
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExportData.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExportData.svelte
index f6c8479b4e..5955cc762d 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExportData.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExportData.svelte
@@ -1,9 +1,9 @@
@@ -67,13 +95,29 @@
options={componentOptions}
on:change={() => (parameters.columns = [])}
/>
+
+
- {
+ const columns = e.detail
+ parameters.customHeaders = columns.reduce((headerMap, column) => {
+ return {
+ [column.name]: column.displayName,
+ ...headerMap,
+ }
+ }, {})
+ }}
/>
@@ -97,8 +141,8 @@
.params {
display: grid;
column-gap: var(--spacing-xs);
- row-gap: var(--spacing-s);
- grid-template-columns: 90px 1fr;
+ row-gap: var(--spacing-m);
+ grid-template-columns: 90px 1fr 90px;
align-items: center;
}
diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte
index 2b9fa573c2..742ab785a1 100644
--- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte
+++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte
@@ -29,6 +29,12 @@
allowLinks: true,
})
+ $: {
+ value = (value || []).filter(
+ column => (schema || {})[column.name || column] !== undefined
+ )
+ }
+
const getText = value => {
if (!value?.length) {
return "All columns"
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index b2068ad152..68478b76ac 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -341,7 +341,11 @@ const exportDataHandler = async action => {
tableId: selection.tableId,
rows: selection.selectedRows,
format: action.parameters.type,
- columns: action.parameters.columns,
+ columns: action.parameters.columns?.map(
+ column => column.name || column
+ ),
+ delimiter: action.parameters.delimiter,
+ customHeaders: action.parameters.customHeaders,
})
download(
new Blob([data], { type: "text/plain" }),
diff --git a/packages/frontend-core/src/api/rows.js b/packages/frontend-core/src/api/rows.js
index 79f837e864..0a0d48da43 100644
--- a/packages/frontend-core/src/api/rows.js
+++ b/packages/frontend-core/src/api/rows.js
@@ -89,13 +89,24 @@ export const buildRowEndpoints = API => ({
* @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)
+ * @param delimiter how values should be separated in a CSV (default is comma)
*/
- exportRows: async ({ tableId, rows, format, columns, search }) => {
+ exportRows: async ({
+ tableId,
+ rows,
+ format,
+ columns,
+ search,
+ delimiter,
+ customHeaders,
+ }) => {
return await API.post({
url: `/api/${tableId}/rows/exportRows?format=${format}`,
body: {
rows,
columns,
+ delimiter,
+ customHeaders,
...search,
},
parseResponse: async response => {
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 1ad8a2a695..ec56919d12 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -223,7 +223,8 @@ export const exportRows = async (
const format = ctx.query.format
- const { rows, columns, query, sort, sortOrder } = ctx.request.body
+ const { rows, columns, query, sort, sortOrder, delimiter, customHeaders } =
+ ctx.request.body
if (typeof format !== "string" || !exporters.isFormat(format)) {
ctx.throw(
400,
@@ -241,6 +242,8 @@ export const exportRows = async (
query,
sort,
sortOrder,
+ delimiter,
+ customHeaders,
})
ctx.attachment(fileName)
ctx.body = apiFileReturn(content)
diff --git a/packages/server/src/api/controllers/view/exporters.ts b/packages/server/src/api/controllers/view/exporters.ts
index d6caff6035..3b5f951dca 100644
--- a/packages/server/src/api/controllers/view/exporters.ts
+++ b/packages/server/src/api/controllers/view/exporters.ts
@@ -1,7 +1,19 @@
import { Row, TableSchema } from "@budibase/types"
-export function csv(headers: string[], rows: Row[]) {
- let csv = headers.map(key => `"${key}"`).join(",")
+function getHeaders(
+ headers: string[],
+ customHeaders: { [key: string]: string }
+) {
+ return headers.map(header => `"${customHeaders[header] || header}"`)
+}
+
+export function csv(
+ headers: string[],
+ rows: Row[],
+ delimiter: string = ",",
+ customHeaders: { [key: string]: string } = {}
+) {
+ let csv = getHeaders(headers, customHeaders).join(delimiter)
for (let row of rows) {
csv = `${csv}\n${headers
@@ -15,7 +27,7 @@ export function csv(headers: string[], rows: Row[]) {
: ""
return val.trim()
})
- .join(",")}`
+ .join(delimiter)}`
}
return csv
}
diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts
index 4b71179839..8b24f9bc5f 100644
--- a/packages/server/src/sdk/app/rows/search.ts
+++ b/packages/server/src/sdk/app/rows/search.ts
@@ -36,11 +36,13 @@ export async function search(options: SearchParams): Promise<{
export interface ExportRowsParams {
tableId: string
format: Format
+ delimiter?: string
rowIds?: string[]
columns?: string[]
query?: SearchFilters
sort?: string
sortOrder?: SortOrder
+ customHeaders?: { [key: string]: string }
}
export interface ExportRowsResult {
diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts
index 8465f997e3..e2d1a1b32c 100644
--- a/packages/server/src/sdk/app/rows/search/external.ts
+++ b/packages/server/src/sdk/app/rows/search/external.ts
@@ -101,7 +101,17 @@ export async function search(options: SearchParams) {
export async function exportRows(
options: ExportRowsParams
): Promise {
- const { tableId, format, columns, rowIds, query, sort, sortOrder } = options
+ const {
+ tableId,
+ format,
+ columns,
+ rowIds,
+ query,
+ sort,
+ sortOrder,
+ delimiter,
+ customHeaders,
+ } = options
const { datasourceId, tableName } = breakExternalTableId(tableId)
let requestQuery: SearchFilters = {}
@@ -153,12 +163,17 @@ export async function exportRows(
rows = result.rows
}
- let exportRows = cleanExportRows(rows, schema, format, columns)
+ let exportRows = cleanExportRows(rows, schema, format, columns, customHeaders)
let content: string
switch (format) {
case exporters.Format.CSV:
- content = exporters.csv(headers ?? Object.keys(schema), exportRows)
+ content = exporters.csv(
+ headers ?? Object.keys(schema),
+ exportRows,
+ delimiter,
+ customHeaders
+ )
break
case exporters.Format.JSON:
content = exporters.json(exportRows)
diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts
index 22cb3985b7..2d3c32e02e 100644
--- a/packages/server/src/sdk/app/rows/search/internal.ts
+++ b/packages/server/src/sdk/app/rows/search/internal.ts
@@ -84,7 +84,17 @@ export async function search(options: SearchParams) {
export async function exportRows(
options: ExportRowsParams
): Promise {
- const { tableId, format, rowIds, columns, query, sort, sortOrder } = options
+ const {
+ tableId,
+ format,
+ rowIds,
+ columns,
+ query,
+ sort,
+ sortOrder,
+ delimiter,
+ customHeaders,
+ } = options
const db = context.getAppDB()
const table = await sdk.tables.getTable(tableId)
@@ -124,11 +134,16 @@ export async function exportRows(
rows = result
}
- let exportRows = cleanExportRows(rows, schema, format, columns)
+ let exportRows = cleanExportRows(rows, schema, format, columns, customHeaders)
if (format === Format.CSV) {
return {
fileName: "export.csv",
- content: csv(headers ?? Object.keys(rows[0]), exportRows),
+ content: csv(
+ headers ?? Object.keys(rows[0]),
+ exportRows,
+ delimiter,
+ customHeaders
+ ),
}
} else if (format === Format.JSON) {
return {
diff --git a/packages/server/src/sdk/app/rows/utils.ts b/packages/server/src/sdk/app/rows/utils.ts
index 14868a4013..0ff85f40ac 100644
--- a/packages/server/src/sdk/app/rows/utils.ts
+++ b/packages/server/src/sdk/app/rows/utils.ts
@@ -16,7 +16,8 @@ export function cleanExportRows(
rows: any[],
schema: TableSchema,
format: string,
- columns?: string[]
+ columns?: string[],
+ customHeaders: { [key: string]: string } = {}
) {
let cleanRows = [...rows]
@@ -44,11 +45,27 @@ export function cleanExportRows(
}
}
}
+ } else if (format === Format.JSON) {
+ // Replace row keys with custom headers
+ for (let row of cleanRows) {
+ renameKeys(customHeaders, row)
+ }
}
return cleanRows
}
+function renameKeys(keysMap: { [key: string]: any }, row: any) {
+ for (const key in keysMap) {
+ Object.defineProperty(
+ row,
+ keysMap[key],
+ Object.getOwnPropertyDescriptor(row, key) || {}
+ )
+ delete row[key]
+ }
+}
+
function isForeignKey(key: string, table: Table) {
const relationships = Object.values(table.schema).filter(isRelationshipColumn)
return relationships.some(
diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts
index dad3286754..14e28e4a01 100644
--- a/packages/types/src/api/web/app/rows.ts
+++ b/packages/types/src/api/web/app/rows.ts
@@ -37,6 +37,8 @@ export interface ExportRowsRequest {
query?: SearchFilters
sort?: string
sortOrder?: SortOrder
+ delimiter?: string
+ customHeaders?: { [key: string]: string }
}
export type ExportRowsResponse = ReadStream
From b1dd8999cb9ff7d542277fe02d33586064abfdf1 Mon Sep 17 00:00:00 2001
From: Budibase Staging Release Bot <>
Date: Tue, 27 Feb 2024 09:33:44 +0000
Subject: [PATCH 2/2] Bump version to 2.20.11
---
lerna.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lerna.json b/lerna.json
index 54e106cd5a..623fbf6d43 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.20.10",
+ "version": "2.20.11",
"npmClient": "yarn",
"packages": [
"packages/*",