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/*",