Merge pull request #11236 from Budibase/feature/delete-multiple-button-action
Delete multiple rows with Delete Row button action
This commit is contained in:
commit
f1d7789651
11 changed files with 273 additions and 65 deletions
|
@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => {
|
||||||
readableBinding: `${table._instanceName}.Selected rows`,
|
readableBinding: `${table._instanceName}.Selected rows`,
|
||||||
category: "Selected rows",
|
category: "Selected rows",
|
||||||
icon: "ViewRow",
|
icon: "ViewRow",
|
||||||
|
display: { name: table._instanceName },
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => {
|
||||||
)}.${makePropSafe("selectedRows")}`,
|
)}.${makePropSafe("selectedRows")}`,
|
||||||
readableBinding: `${block._instanceName}.Selected rows`,
|
readableBinding: `${block._instanceName}.Selected rows`,
|
||||||
category: "Selected rows",
|
category: "Selected rows",
|
||||||
|
display: { name: block._instanceName },
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,11 @@
|
||||||
|
|
||||||
return allBindings
|
return allBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toDisplay = eventKey => {
|
||||||
|
const type = actionTypes.find(action => action.name == eventKey)
|
||||||
|
return type?.displayName || type?.name
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
|
@ -231,7 +236,9 @@
|
||||||
<ul>
|
<ul>
|
||||||
{#each category as actionType}
|
{#each category as actionType}
|
||||||
<li on:click={onAddAction(actionType)}>
|
<li on:click={onAddAction(actionType)}>
|
||||||
<span class="action-name">{actionType.name}</span>
|
<span class="action-name">
|
||||||
|
{actionType.displayName || actionType.name}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -262,7 +269,7 @@
|
||||||
>
|
>
|
||||||
<Icon name="DragHandle" size="XL" />
|
<Icon name="DragHandle" size="XL" />
|
||||||
<div class="action-header">
|
<div class="action-header">
|
||||||
{index + 1}. {action[EVENT_TYPE_KEY]}
|
{index + 1}. {toDisplay(action[EVENT_TYPE_KEY])}
|
||||||
</div>
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
name="Close"
|
name="Close"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label, Checkbox, Input } from "@budibase/bbui"
|
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
|
@ -10,47 +10,59 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<Label>Table</Label>
|
<Body size="small">Please specify one or more rows to delete.</Body>
|
||||||
<Select
|
<div class="params">
|
||||||
bind:value={parameters.tableId}
|
<Label>Table</Label>
|
||||||
options={tableOptions}
|
<Select
|
||||||
getOptionLabel={table => table.name}
|
bind:value={parameters.tableId}
|
||||||
getOptionValue={table => table._id}
|
options={tableOptions}
|
||||||
/>
|
getOptionLabel={table => table.name}
|
||||||
|
getOptionValue={table => table._id}
|
||||||
<Label small>Row ID</Label>
|
|
||||||
<DrawerBindableInput
|
|
||||||
{bindings}
|
|
||||||
title="Row ID to delete"
|
|
||||||
value={parameters.rowId}
|
|
||||||
on:change={value => (parameters.rowId = value.detail)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Label small />
|
|
||||||
<Checkbox
|
|
||||||
text="Do not display default notification"
|
|
||||||
bind:value={parameters.notificationOverride}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
|
||||||
|
|
||||||
{#if parameters.confirm}
|
|
||||||
<Label small>Confirm text</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Are you sure you want to delete this row?"
|
|
||||||
bind:value={parameters.confirmText}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
|
||||||
|
<Label small>Row IDs</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
{bindings}
|
||||||
|
title="Rows to delete"
|
||||||
|
value={parameters.rowId}
|
||||||
|
on:change={value => (parameters.rowId = value.detail)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label small />
|
||||||
|
<Checkbox
|
||||||
|
text="Do not display default notification"
|
||||||
|
bind:value={parameters.notificationOverride}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
||||||
|
|
||||||
|
{#if parameters.confirm}
|
||||||
|
<Label small>Confirm text</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Are you sure you want to delete?"
|
||||||
|
bind:value={parameters.confirmText}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.params {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-l);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: 60px 1fr;
|
grid-template-columns: 60px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Delete Row",
|
"name": "Delete Row",
|
||||||
|
"displayName": "Delete Rows",
|
||||||
"type": "data",
|
"type": "data",
|
||||||
"component": "DeleteRow"
|
"component": "DeleteRow"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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) => {
|
const getFields = (schema, customColumns, showAutoColumns) => {
|
||||||
// Check for an invalid column selection
|
// Check for an invalid column selection
|
||||||
let invalid = false
|
let invalid = false
|
||||||
|
|
|
@ -102,12 +102,46 @@ const fetchRowHandler = async action => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRowHandler = async action => {
|
const deleteRowHandler = async action => {
|
||||||
const { tableId, revId, rowId, notificationOverride } = action.parameters
|
const { tableId, rowId: rowConfig, notificationOverride } = action.parameters
|
||||||
if (tableId && rowId) {
|
|
||||||
|
if (tableId && rowConfig) {
|
||||||
try {
|
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) {
|
if (!notificationOverride) {
|
||||||
notificationStore.actions.success("Row deleted")
|
notificationStore.actions.success(
|
||||||
|
resp?.length == 1 ? "Row deleted" : `${resp.length} Rows deleted`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
|
@ -115,8 +149,10 @@ const deleteRowHandler = async action => {
|
||||||
invalidateRelationships: true,
|
invalidateRelationships: true,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Abort next actions
|
console.error(error)
|
||||||
return false
|
notificationStore.actions.error(
|
||||||
|
"An error occurred while executing the query"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// makes sure that the user doesn't need to pass in the type, tableId or _id params for
|
||||||
// the call to be correct
|
// the call to be correct
|
||||||
function fixRow(row: Row, params: any) {
|
export function fixRow(row: Row, params: any) {
|
||||||
if (!params || !row) {
|
if (!params || !row) {
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,11 @@ import * as external from "./external"
|
||||||
import { isExternalTable } from "../../../integrations/utils"
|
import { isExternalTable } from "../../../integrations/utils"
|
||||||
import {
|
import {
|
||||||
Ctx,
|
Ctx,
|
||||||
|
UserCtx,
|
||||||
|
DeleteRowRequest,
|
||||||
|
DeleteRow,
|
||||||
|
DeleteRows,
|
||||||
|
Row,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SortType,
|
SortType,
|
||||||
|
@ -11,6 +16,8 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import { gridSocket } from "../../../websockets"
|
import { gridSocket } from "../../../websockets"
|
||||||
|
import { addRev } from "../public/utils"
|
||||||
|
import { fixRow } from "../public/rows"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as exporters from "../view/exporters"
|
import * as exporters from "../view/exporters"
|
||||||
import { apiFileReturn } from "../../../utilities/fileSystem"
|
import { apiFileReturn } from "../../../utilities/fileSystem"
|
||||||
|
@ -104,35 +111,83 @@ export async function find(ctx: any) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function destroy(ctx: any) {
|
function isDeleteRows(input: any): input is DeleteRows {
|
||||||
const appId = ctx.appId
|
return input.rows !== undefined && Array.isArray(input.rows)
|
||||||
const inputs = ctx.request.body
|
}
|
||||||
|
|
||||||
|
function isDeleteRow(input: any): input is DeleteRow {
|
||||||
|
return input._id !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processDeleteRowsRequest(ctx: UserCtx<DeleteRowRequest>) {
|
||||||
|
let request = ctx.request.body as DeleteRows
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
let response, row
|
|
||||||
if (inputs.rows) {
|
const processedRows = request.rows.map(row => {
|
||||||
let { rows } = await quotas.addQuery<any>(
|
let processedRow: Row = typeof row == "string" ? { _id: row } : row
|
||||||
() => pickApi(tableId).bulkDestroy(ctx),
|
return !processedRow._rev
|
||||||
{
|
? addRev(fixRow(processedRow, ctx.params), tableId)
|
||||||
datasourceId: tableId,
|
: fixRow(processedRow, ctx.params)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
await quotas.removeRows(rows.length)
|
return await Promise.all(processedRows)
|
||||||
response = rows
|
}
|
||||||
for (let row of rows) {
|
|
||||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
|
||||||
gridSocket?.emitRowDeletion(ctx, row._id!)
|
const tableId = utils.getTableId(ctx)
|
||||||
}
|
const appId = ctx.appId
|
||||||
} else {
|
|
||||||
let resp = await quotas.addQuery<any>(() => pickApi(tableId).destroy(ctx), {
|
let deleteRequest = ctx.request.body as DeleteRows
|
||||||
|
|
||||||
|
const rowDeletes: Row[] = await processDeleteRowsRequest(ctx)
|
||||||
|
deleteRequest.rows = rowDeletes
|
||||||
|
|
||||||
|
let { rows } = await quotas.addQuery<any>(
|
||||||
|
() => pickApi(tableId).bulkDestroy(ctx),
|
||||||
|
{
|
||||||
datasourceId: tableId,
|
datasourceId: tableId,
|
||||||
})
|
}
|
||||||
await quotas.removeRow()
|
)
|
||||||
response = resp.response
|
await quotas.removeRows(rows.length)
|
||||||
row = resp.row
|
|
||||||
|
for (let row of rows) {
|
||||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
||||||
gridSocket?.emitRowDeletion(ctx, row._id!)
|
gridSocket?.emitRowDeletion(ctx, row._id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
|
||||||
|
const appId = ctx.appId
|
||||||
|
const tableId = utils.getTableId(ctx)
|
||||||
|
|
||||||
|
let resp = await quotas.addQuery<any>(() => 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<DeleteRowRequest>) {
|
||||||
|
let response, row
|
||||||
ctx.status = 200
|
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
|
// for automations include the row that was deleted
|
||||||
ctx.row = row || {}
|
ctx.row = row || {}
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
|
|
|
@ -519,6 +519,81 @@ describe("/rows", () => {
|
||||||
await assertRowUsage(rowUsage - 2)
|
await assertRowUsage(rowUsage - 2)
|
||||||
await assertQueryUsage(queryUsage + 1)
|
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", () => {
|
describe("fetchView", () => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export * from "./backup"
|
export * from "./backup"
|
||||||
export * from "./datasource"
|
export * from "./datasource"
|
||||||
|
export * from "./row"
|
||||||
export * from "./view"
|
export * from "./view"
|
||||||
export * from "./rows"
|
export * from "./rows"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
|
11
packages/types/src/api/web/app/row.ts
Normal file
11
packages/types/src/api/web/app/row.ts
Normal file
|
@ -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
|
Loading…
Reference in a new issue