diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index e9c8643bce..bbe116721a 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => { readableBinding: `${table._instanceName}.Selected rows`, category: "Selected rows", icon: "ViewRow", + display: { name: table._instanceName }, })) ) @@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => { )}.${makePropSafe("selectedRows")}`, readableBinding: `${block._instanceName}.Selected rows`, category: "Selected rows", + display: { name: block._instanceName }, })) ) } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte index d9111d4943..ef6410abca 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte @@ -206,6 +206,11 @@ return allBindings } + + const toDisplay = eventKey => { + const type = actionTypes.find(action => action.name == eventKey) + return type?.displayName || type?.name + } @@ -231,7 +236,9 @@ @@ -262,7 +269,7 @@ >
- {index + 1}. {action[EVENT_TYPE_KEY]} + {index + 1}. {toDisplay(action[EVENT_TYPE_KEY])}
- import { Select, Label, Checkbox, Input } from "@budibase/bbui" + import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui" import { tables } from "stores/backend" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" @@ -10,47 +10,59 @@
- - Please specify one or more rows to delete. +
+ + + {/if} +
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json index 2ec7235c59..6ed545f541 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json @@ -24,6 +24,7 @@ }, { "name": "Delete Row", + "displayName": "Delete Rows", "type": "data", "component": "DeleteRow" }, diff --git a/packages/client/src/components/app/table/Table.svelte b/packages/client/src/components/app/table/Table.svelte index 248151a7a2..0ed76317db 100644 --- a/packages/client/src/components/app/table/Table.svelte +++ b/packages/client/src/components/app/table/Table.svelte @@ -47,6 +47,14 @@ ) } + // If the data changes, double check that the selected elements are still present. + $: if (data) { + let rowIds = data.map(row => row._id) + if (rowIds.length) { + selectedRows = selectedRows.filter(row => rowIds.includes(row._id)) + } + } + const getFields = (schema, customColumns, showAutoColumns) => { // Check for an invalid column selection let invalid = false diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 0405dc7d55..4fdda2a076 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -102,12 +102,46 @@ const fetchRowHandler = async action => { } const deleteRowHandler = async action => { - const { tableId, revId, rowId, notificationOverride } = action.parameters - if (tableId && rowId) { + const { tableId, rowId: rowConfig, notificationOverride } = action.parameters + + if (tableId && rowConfig) { try { - await API.deleteRow({ tableId, rowId, revId }) + let requestConfig + + let parsedRowConfig = [] + if (typeof rowConfig === "string") { + try { + parsedRowConfig = JSON.parse(rowConfig) + } catch (e) { + parsedRowConfig = rowConfig + .split(",") + .map(id => id.trim()) + .filter(id => id) + } + } else { + parsedRowConfig = rowConfig + } + + if ( + typeof parsedRowConfig === "object" && + parsedRowConfig.constructor === Object + ) { + requestConfig = [parsedRowConfig] + } else if (Array.isArray(parsedRowConfig)) { + requestConfig = parsedRowConfig + } + + if (!requestConfig.length) { + notificationStore.actions.warning("No valid rows were supplied") + return false + } + + const resp = await API.deleteRows({ tableId, rows: requestConfig }) + if (!notificationOverride) { - notificationStore.actions.success("Row deleted") + notificationStore.actions.success( + resp?.length == 1 ? "Row deleted" : `${resp.length} Rows deleted` + ) } // Refresh related datasources @@ -115,8 +149,10 @@ const deleteRowHandler = async action => { invalidateRelationships: true, }) } catch (error) { - // Abort next actions - return false + console.error(error) + notificationStore.actions.error( + "An error occurred while executing the query" + ) } } } diff --git a/packages/server/src/api/controllers/public/rows.ts b/packages/server/src/api/controllers/public/rows.ts index df856f1fe0..39cf85a2a3 100644 --- a/packages/server/src/api/controllers/public/rows.ts +++ b/packages/server/src/api/controllers/public/rows.ts @@ -5,7 +5,7 @@ import { convertBookmark } from "../../../utilities" // makes sure that the user doesn't need to pass in the type, tableId or _id params for // the call to be correct -function fixRow(row: Row, params: any) { +export function fixRow(row: Row, params: any) { if (!params || !row) { return row } diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 4a9047d2cd..79cd5fbfe0 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -4,6 +4,11 @@ import * as external from "./external" import { isExternalTable } from "../../../integrations/utils" import { Ctx, + UserCtx, + DeleteRowRequest, + DeleteRow, + DeleteRows, + Row, SearchResponse, SortOrder, SortType, @@ -11,6 +16,8 @@ import { } from "@budibase/types" import * as utils from "./utils" import { gridSocket } from "../../../websockets" +import { addRev } from "../public/utils" +import { fixRow } from "../public/rows" import sdk from "../../../sdk" import * as exporters from "../view/exporters" import { apiFileReturn } from "../../../utilities/fileSystem" @@ -104,35 +111,83 @@ export async function find(ctx: any) { }) } -export async function destroy(ctx: any) { - const appId = ctx.appId - const inputs = ctx.request.body +function isDeleteRows(input: any): input is DeleteRows { + return input.rows !== undefined && Array.isArray(input.rows) +} + +function isDeleteRow(input: any): input is DeleteRow { + return input._id !== undefined +} + +async function processDeleteRowsRequest(ctx: UserCtx) { + let request = ctx.request.body as DeleteRows const tableId = utils.getTableId(ctx) - let response, row - if (inputs.rows) { - let { rows } = await quotas.addQuery( - () => pickApi(tableId).bulkDestroy(ctx), - { - datasourceId: tableId, - } - ) - await quotas.removeRows(rows.length) - response = rows - for (let row of rows) { - ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) - gridSocket?.emitRowDeletion(ctx, row._id!) - } - } else { - let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), { + + const processedRows = request.rows.map(row => { + let processedRow: Row = typeof row == "string" ? { _id: row } : row + return !processedRow._rev + ? addRev(fixRow(processedRow, ctx.params), tableId) + : fixRow(processedRow, ctx.params) + }) + + return await Promise.all(processedRows) +} + +async function deleteRows(ctx: UserCtx) { + const tableId = utils.getTableId(ctx) + const appId = ctx.appId + + let deleteRequest = ctx.request.body as DeleteRows + + const rowDeletes: Row[] = await processDeleteRowsRequest(ctx) + deleteRequest.rows = rowDeletes + + let { rows } = await quotas.addQuery( + () => pickApi(tableId).bulkDestroy(ctx), + { datasourceId: tableId, - }) - await quotas.removeRow() - response = resp.response - row = resp.row + } + ) + await quotas.removeRows(rows.length) + + for (let row of rows) { ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) gridSocket?.emitRowDeletion(ctx, row._id!) } + + return rows +} + +async function deleteRow(ctx: UserCtx) { + const appId = ctx.appId + const tableId = utils.getTableId(ctx) + + let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), { + datasourceId: tableId, + }) + await quotas.removeRow() + + ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row) + gridSocket?.emitRowDeletion(ctx, resp.row._id) + + return resp +} + +export async function destroy(ctx: UserCtx) { + let response, row ctx.status = 200 + + if (isDeleteRows(ctx.request.body)) { + response = await deleteRows(ctx) + } else if (isDeleteRow(ctx.request.body)) { + const deleteResp = await deleteRow(ctx) + response = deleteResp.response + row = deleteResp.row + } else { + ctx.status = 400 + response = { message: "Invalid delete rows request" } + } + // for automations include the row that was deleted ctx.row = row || {} ctx.body = response diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index c872128b08..d7279148c7 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -519,6 +519,81 @@ describe("/rows", () => { await assertRowUsage(rowUsage - 2) await assertQueryUsage(queryUsage + 1) }) + + it("should be able to delete a variety of row set types", async () => { + const row1 = await config.createRow() + const row2 = await config.createRow() + const row3 = await config.createRow() + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + const res = await request + .delete(`/api/${table._id}/rows`) + .send({ + rows: [row1, row2._id, { _id: row3._id }], + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + expect(res.body.length).toEqual(3) + await loadRow(row1._id!, table._id!, 404) + await assertRowUsage(rowUsage - 3) + await assertQueryUsage(queryUsage + 1) + }) + + it("should accept a valid row object and delete the row", async () => { + const row1 = await config.createRow() + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + const res = await request + .delete(`/api/${table._id}/rows`) + .send(row1) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + + expect(res.body.id).toEqual(row1._id) + await loadRow(row1._id!, table._id!, 404) + await assertRowUsage(rowUsage - 1) + await assertQueryUsage(queryUsage + 1) + }) + + it("Should ignore malformed/invalid delete requests", async () => { + const rowUsage = await getRowUsage() + const queryUsage = await getQueryUsage() + + const res = await request + .delete(`/api/${table._id}/rows`) + .send({ not: "valid" }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + + expect(res.body.message).toEqual("Invalid delete rows request") + + const res2 = await request + .delete(`/api/${table._id}/rows`) + .send({ rows: 123 }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + + expect(res2.body.message).toEqual("Invalid delete rows request") + + const res3 = await request + .delete(`/api/${table._id}/rows`) + .send("invalid") + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + + expect(res3.body.message).toEqual("Invalid delete rows request") + + await assertRowUsage(rowUsage) + await assertQueryUsage(queryUsage) + }) }) describe("fetchView", () => { diff --git a/packages/types/src/api/web/app/index.ts b/packages/types/src/api/web/app/index.ts index 9c4aa35f57..e7b4b87aa9 100644 --- a/packages/types/src/api/web/app/index.ts +++ b/packages/types/src/api/web/app/index.ts @@ -1,5 +1,6 @@ export * from "./backup" export * from "./datasource" +export * from "./row" export * from "./view" export * from "./rows" export * from "./table" diff --git a/packages/types/src/api/web/app/row.ts b/packages/types/src/api/web/app/row.ts new file mode 100644 index 0000000000..f9623a3daf --- /dev/null +++ b/packages/types/src/api/web/app/row.ts @@ -0,0 +1,11 @@ +import { Row } from "../../../documents/app/row" + +export interface DeleteRows { + rows: (Row | string)[] +} + +export interface DeleteRow { + _id: string +} + +export type DeleteRowRequest = DeleteRows | DeleteRow