Add button action allowing for export of client row selection

This commit is contained in:
Peter Clement 2022-03-07 12:06:11 +00:00
parent 49420a6818
commit ea63b9b065
14 changed files with 155 additions and 24 deletions

View File

@ -331,7 +331,9 @@ const getSelectedRowsBindings = asset => {
bindings = bindings.concat( bindings = bindings.concat(
tables.map(table => ({ tables.map(table => ({
type: "context", type: "context",
runtimeBinding: `${safeState}.${makePropSafe(table._id)}`, runtimeBinding: `${safeState}.${makePropSafe(table._id)}.${makePropSafe(
"selectedRows"
)}`,
readableBinding: `${table._instanceName}.Selected rows`, readableBinding: `${table._instanceName}.Selected rows`,
})) }))
) )
@ -343,7 +345,9 @@ const getSelectedRowsBindings = asset => {
bindings = bindings.concat( bindings = bindings.concat(
tableBlocks.map(block => ({ tableBlocks.map(block => ({
type: "context", type: "context",
runtimeBinding: `${safeState}.${makePropSafe(block._id + "-table")}`, runtimeBinding: `${safeState}.${makePropSafe(
block._id + "-table"
)}.${makePropSafe("selectedRows")}`,
readableBinding: `${block._instanceName}.Selected rows`, readableBinding: `${block._instanceName}.Selected rows`,
})) }))
) )

View File

@ -0,0 +1,71 @@
<script>
import { Label, Select, Body } from "@budibase/bbui"
import { tables } from "stores/backend"
import { onMount } from "svelte"
export let parameters
$: tableOptions = $tables.list || []
const FORMATS = [
{
label: "CSV",
value: "csv",
},
{
label: "JSON",
value: "json",
},
]
onMount(() => {
if (!parameters.type) {
parameters.type = "csv"
}
})
</script>
<div class="root">
<Body size="S">
Choose the table that you would like to export your row selection from.
<br />
Please ensure you have enabled row selection in the table settings
</Body>
<div class="params">
<Label small>Table</Label>
<Select
bind:value={parameters.tableId}
options={tableOptions}
getOptionLabel={option => option.name}
getOptionValue={option => option._id}
/>
<Label small>Type</Label>
<Select bind:value={parameters.type} options={FORMATS} />
</div>
</div>
<style>
.root {
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.root :global(p) {
line-height: 1.5;
}
.params {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 100px 1fr;
align-items: center;
}
</style>

View File

@ -12,3 +12,4 @@ export { default as UpdateState } from "./UpdateState.svelte"
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte" export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
export { default as DuplicateRow } from "./DuplicateRow.svelte" export { default as DuplicateRow } from "./DuplicateRow.svelte"
export { default as S3Upload } from "./S3Upload.svelte" export { default as S3Upload } from "./S3Upload.svelte"
export { default as ExportData } from "./ExportData.svelte"

View File

@ -80,6 +80,10 @@
"value": "publicUrl" "value": "publicUrl"
} }
] ]
},
{
"name": "Export Data",
"component": "ExportData"
} }
] ]
} }

View File

@ -32,6 +32,7 @@
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1", "apexcharts": "^3.22.1",
"dayjs": "^1.10.5", "dayjs": "^1.10.5",
"downloadjs": "1.4.7",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"rollup-plugin-polyfill-node": "^0.8.0", "rollup-plugin-polyfill-node": "^0.8.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",

View File

@ -42,6 +42,7 @@
$: { $: {
rowSelectionStore.actions.updateSelection( rowSelectionStore.actions.updateSelection(
$component.id, $component.id,
selectedRows.length ? selectedRows[0].tableId : "",
selectedRows.map(row => row._id) selectedRows.map(row => row._id)
) )
} }

View File

@ -1,20 +1,29 @@
import { writable } from "svelte/store" import { get, writable } from "svelte/store"
const createRowSelectionStore = () => { const createRowSelectionStore = () => {
const store = writable({}) const store = writable({})
function updateSelection(componentId, selectedRows) { function updateSelection(componentId, tableId, selectedRows) {
store.update(state => { store.update(state => {
state[componentId] = [...selectedRows] state[componentId] = { tableId: tableId, selectedRows: selectedRows }
return state 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 { return {
subscribe: store.subscribe, subscribe: store.subscribe,
set: store.set, set: store.set,
actions: { actions: {
updateSelection, updateSelection,
getSelection,
}, },
} }
} }

View File

@ -1,4 +1,5 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import download from "downloadjs"
import { import {
routeStore, routeStore,
builderStore, builderStore,
@ -8,6 +9,7 @@ import {
notificationStore, notificationStore,
dataSourceStore, dataSourceStore,
uploadStore, uploadStore,
rowSelectionStore,
} from "stores" } from "stores"
import { API } from "api" import { API } from "api"
import { ActionTypes } from "constants" 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 = { const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Duplicate Row"]: duplicateRowHandler, ["Duplicate Row"]: duplicateRowHandler,
@ -254,6 +276,7 @@ const handlerMap = {
["Change Form Step"]: changeFormStepHandler, ["Change Form Step"]: changeFormStepHandler,
["Update State"]: updateStateHandler, ["Update State"]: updateStateHandler,
["Upload File to S3"]: s3UploadHandler, ["Upload File to S3"]: s3UploadHandler,
["Export Data"]: exportDataHandler,
} }
const confirmTextMap = { const confirmTextMap = {

View File

@ -464,6 +464,11 @@ domutils@^2.6.0:
domelementtype "^2.2.0" domelementtype "^2.2.0"
domhandler "^4.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: electron-to-chromium@^1.3.896:
version "1.3.900" version "1.3.900"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5"

View File

@ -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,
},
})
},
}) })

View File

@ -164,14 +164,15 @@ exports.exportRows = async ctx => {
ctx.request.body = { ctx.request.body = {
query: { query: {
oneOf: { oneOf: {
[table.primaryDisplay]: ctx.request.body.map(id => breakRowIdField(id)[0]) [table.primaryDisplay]: ctx.request.body.map(
id => breakRowIdField(id)[0]
),
}, },
}, },
} }
return exports.search(ctx) return exports.search(ctx)
} }
exports.fetchEnrichedRow = async ctx => { exports.fetchEnrichedRow = async ctx => {
const id = ctx.params.rowId const id = ctx.params.rowId
const tableId = ctx.params.tableId const tableId = ctx.params.tableId

View File

@ -145,5 +145,4 @@ exports.export = async function (ctx) {
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }
} }

View File

@ -365,7 +365,7 @@ exports.validate = async ctx => {
exports.exportRows = async ctx => { exports.exportRows = async ctx => {
const db = getAppDB() const db = getAppDB()
const table = await db.get(ctx.params.tableId) const table = await db.get(ctx.params.tableId)
const rowIds = ctx.request.body const rowIds = ctx.request.body.rows
let response = ( let response = (
await db.allDocs({ await db.allDocs({
include_docs: true, include_docs: true,
@ -378,7 +378,6 @@ exports.exportRows = async ctx => {
return rows return rows
} }
exports.fetchEnrichedRow = async ctx => { exports.fetchEnrichedRow = async ctx => {
const db = getAppDB() const db = getAppDB()
const tableId = ctx.params.tableId const tableId = ctx.params.tableId

View File

@ -253,7 +253,7 @@ router
) )
/** /**
* @api {post} /api/:tableId/rows/export Export Rows * @api {post} /api/:tableId/rows/exportRows Export Rows
* @apiName Export rows * @apiName Export rows
* @apiGroup rows * @apiGroup rows
* @apiPermission table write access * @apiPermission table write access
@ -266,12 +266,11 @@ router
* @apiSuccess {object[]|object} * @apiSuccess {object[]|object}
*/ */
.post( .post(
"/api/:tableId/rows/export", "/api/:tableId/rows/exportRows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage, usage,
rowController.export rowController.export
) )
module.exports = router module.exports = router