diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 60cce2b1fd..5b9bebcbf5 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -331,7 +331,9 @@ const getSelectedRowsBindings = asset => { bindings = bindings.concat( tables.map(table => ({ type: "context", - runtimeBinding: `${safeState}.${makePropSafe(table._id)}`, + runtimeBinding: `${safeState}.${makePropSafe(table._id)}.${makePropSafe( + "selectedRows" + )}`, readableBinding: `${table._instanceName}.Selected rows`, })) ) @@ -343,7 +345,9 @@ const getSelectedRowsBindings = asset => { bindings = bindings.concat( tableBlocks.map(block => ({ type: "context", - runtimeBinding: `${safeState}.${makePropSafe(block._id + "-table")}`, + runtimeBinding: `${safeState}.${makePropSafe( + block._id + "-table" + )}.${makePropSafe("selectedRows")}`, readableBinding: `${block._instanceName}.Selected rows`, })) ) 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 new file mode 100644 index 0000000000..93b9a556a6 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/ExportData.svelte @@ -0,0 +1,71 @@ + + +
+ + Choose the table that you would like to export your row selection from. +
+ Please ensure you have enabled row selection in the table settings + + +
+ + +
+
+ + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/index.js index 416ebffb1a..6593c9cbd4 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/index.js +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/actions/index.js @@ -12,3 +12,4 @@ export { default as UpdateState } from "./UpdateState.svelte" export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte" export { default as DuplicateRow } from "./DuplicateRow.svelte" export { default as S3Upload } from "./S3Upload.svelte" +export { default as ExportData } from "./ExportData.svelte" diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json index ecbf0d8065..0f6d3344b2 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json @@ -80,6 +80,10 @@ "value": "publicUrl" } ] + }, + { + "name": "Export Data", + "component": "ExportData" } ] } \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json index 53838962f7..ccd67be70b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -32,6 +32,7 @@ "@spectrum-css/vars": "^3.0.1", "apexcharts": "^3.22.1", "dayjs": "^1.10.5", + "downloadjs": "1.4.7", "regexparam": "^1.3.0", "rollup-plugin-polyfill-node": "^0.8.0", "shortid": "^2.2.15", diff --git a/packages/client/src/components/app/table/Table.svelte b/packages/client/src/components/app/table/Table.svelte index f5f591688f..e8dcd30929 100644 --- a/packages/client/src/components/app/table/Table.svelte +++ b/packages/client/src/components/app/table/Table.svelte @@ -42,6 +42,7 @@ $: { rowSelectionStore.actions.updateSelection( $component.id, + selectedRows.length ? selectedRows[0].tableId : "", selectedRows.map(row => row._id) ) } diff --git a/packages/client/src/stores/rowSelection.js b/packages/client/src/stores/rowSelection.js index 3d1f2038aa..4561557c6f 100644 --- a/packages/client/src/stores/rowSelection.js +++ b/packages/client/src/stores/rowSelection.js @@ -1,20 +1,29 @@ -import { writable } from "svelte/store" +import { get, writable } from "svelte/store" const createRowSelectionStore = () => { const store = writable({}) - function updateSelection(componentId, selectedRows) { + function updateSelection(componentId, tableId, selectedRows) { store.update(state => { - state[componentId] = [...selectedRows] + state[componentId] = { tableId: tableId, selectedRows: selectedRows } return state }) } + function getSelection(tableId) { + const selection = get(store) + const componentId = Object.keys(selection).find( + componentId => selection[componentId].tableId === tableId + ) + return componentId ? selection[componentId] : {} + } + return { subscribe: store.subscribe, set: store.set, actions: { updateSelection, + getSelection, }, } } diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index fec966725b..5d0f4ff71e 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -1,4 +1,5 @@ import { get } from "svelte/store" +import download from "downloadjs" import { routeStore, builderStore, @@ -8,6 +9,7 @@ import { notificationStore, dataSourceStore, uploadStore, + rowSelectionStore, } from "stores" import { API } from "api" import { ActionTypes } from "constants" @@ -239,6 +241,26 @@ const s3UploadHandler = async action => { } } +const exportDataHandler = async action => { + let selection = rowSelectionStore.actions.getSelection( + action.parameters.tableId + ) + if (selection.selectedRows && selection.selectedRows.length > 0) { + try { + const data = await API.exportRows({ + tableId: selection.tableId, + rows: selection.selectedRows, + }) + + download(JSON.stringify(data), `export.${action.parameters.type}`) + } catch (error) { + notificationStore.actions.error("There was an error exporting the data") + } + } else { + notificationStore.actions.error("Please select at least one row") + } +} + const handlerMap = { ["Save Row"]: saveRowHandler, ["Duplicate Row"]: duplicateRowHandler, @@ -254,6 +276,7 @@ const handlerMap = { ["Change Form Step"]: changeFormStepHandler, ["Update State"]: updateStateHandler, ["Upload File to S3"]: s3UploadHandler, + ["Export Data"]: exportDataHandler, } const confirmTextMap = { diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock index 54d3ae755f..405591fd90 100644 --- a/packages/client/yarn.lock +++ b/packages/client/yarn.lock @@ -464,6 +464,11 @@ domutils@^2.6.0: domelementtype "^2.2.0" domhandler "^4.2.0" +downloadjs@1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/downloadjs/-/downloadjs-1.4.7.tgz#f69f96f940e0d0553dac291139865a3cd0101e3c" + integrity sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw= + electron-to-chromium@^1.3.896: version "1.3.900" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5" diff --git a/packages/frontend-core/src/api/rows.js b/packages/frontend-core/src/api/rows.js index 553cf8e0de..1b6efe624f 100644 --- a/packages/frontend-core/src/api/rows.js +++ b/packages/frontend-core/src/api/rows.js @@ -60,4 +60,18 @@ export const buildRowEndpoints = API => ({ }, }) }, + + /** + * Exports rows. + * @param tableId the table ID to export the rows from + * @param rows the array of rows to export + */ + exportRows: async ({ tableId, rows }) => { + return await API.post({ + url: `/api/${tableId}/rows/exportRows`, + body: { + rows, + }, + }) + }, }) diff --git a/packages/server/src/api/controllers/row/external.js b/packages/server/src/api/controllers/row/external.js index 66a1e30ca6..b7c7a2bc6e 100644 --- a/packages/server/src/api/controllers/row/external.js +++ b/packages/server/src/api/controllers/row/external.js @@ -152,6 +152,27 @@ exports.validate = async () => { return { valid: true } } +exports.exportRows = async ctx => { + const { datasourceId, tableName } = breakExternalTableId(ctx.params.tableId) + const db = getAppDB() + const datasource = await db.get(datasourceId) + if (!datasource || !datasource.entities) { + ctx.throw(400, "Datasource has not been configured for plus API.") + } + const tables = datasource.entities + const table = tables[tableName] + ctx.request.body = { + query: { + oneOf: { + [table.primaryDisplay]: ctx.request.body.map( + id => breakRowIdField(id)[0] + ), + }, + }, + } + return exports.search(ctx) +} + exports.fetchEnrichedRow = async ctx => { const id = ctx.params.rowId const tableId = ctx.params.tableId diff --git a/packages/server/src/api/controllers/row/index.js b/packages/server/src/api/controllers/row/index.js index 1d003ebd18..c4a9ac8f06 100644 --- a/packages/server/src/api/controllers/row/index.js +++ b/packages/server/src/api/controllers/row/index.js @@ -137,3 +137,12 @@ exports.fetchEnrichedRow = async function (ctx) { ctx.throw(400, err) } } + +exports.export = async function (ctx) { + const tableId = getTableId(ctx) + try { + ctx.body = await pickApi(tableId).exportRows(ctx) + } catch (err) { + ctx.throw(400, err) + } +} diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 7650ac275f..068a485a8a 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -362,6 +362,22 @@ exports.validate = async ctx => { }) } +exports.exportRows = async ctx => { + const db = getAppDB() + const table = await db.get(ctx.params.tableId) + const rowIds = ctx.request.body.rows + let response = ( + await db.allDocs({ + include_docs: true, + keys: rowIds, + }) + ).rows.map(row => row.doc) + + let rows = await outputProcessing(table, response) + + return rows +} + exports.fetchEnrichedRow = async ctx => { const db = getAppDB() const tableId = ctx.params.tableId diff --git a/packages/server/src/api/routes/row.js b/packages/server/src/api/routes/row.js index 2ceddc779a..272b7d168d 100644 --- a/packages/server/src/api/routes/row.js +++ b/packages/server/src/api/routes/row.js @@ -252,4 +252,25 @@ router rowController.destroy ) + /** + * @api {post} /api/:tableId/rows/exportRows Export Rows + * @apiName Export rows + * @apiGroup rows + * @apiPermission table write access + * @apiDescription This API can export a number of provided rows + * + * @apiParam {string} tableId The ID of the table the row is to be deleted from. + * + * @apiParam (Body) {object[]} [rows] The row IDs which are to be exported + * + * @apiSuccess {object[]|object} + */ + .post( + "/api/:tableId/rows/exportRows", + paramResource("tableId"), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), + usage, + rowController.export + ) + module.exports = router