diff --git a/packages/frontend-core/src/components/grid/layout/NewRow.svelte b/packages/frontend-core/src/components/grid/layout/NewRow.svelte index 68ace8a5b2..f7fd2c661f 100644 --- a/packages/frontend-core/src/components/grid/layout/NewRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewRow.svelte @@ -54,7 +54,10 @@ const newRowIndex = offset ? undefined : 0 let rowToCreate = { ...newRow } delete rowToCreate._isNewRow - const savedRow = await rows.actions.addRow(rowToCreate, newRowIndex) + const savedRow = await rows.actions.addRow({ + row: rowToCreate, + idx: newRowIndex, + }) if (savedRow) { // Reset state clear() diff --git a/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte index db6e82293d..10bd974260 100644 --- a/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte +++ b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte @@ -21,6 +21,7 @@ notifications, hasBudibaseIdentifiers, selectedRowCount, + selectedRows, } = getContext("grid") let anchor @@ -51,6 +52,18 @@ } } + const bulkDuplicate = async () => { + menu.actions.close() + const rowsToDuplicate = Object.keys($selectedRows).map(id => { + return rows.actions.getRow(id) + }) + const newRows = await rows.actions.bulkDuplicate(rowsToDuplicate) + if (newRows[0]) { + const column = $stickyColumn?.name || $columns[0].name + $focusedCellId = getCellID(newRows[0]._id, column) + } + } + const copyToClipboard = async value => { await Helpers.copyToClipboard(value) $notifications.success("Copied to clipboard") @@ -66,8 +79,8 @@ {#if $menu.multiRowMode} Duplicate {$selectedRowCount} rows diff --git a/packages/frontend-core/src/components/grid/stores/datasources/table.js b/packages/frontend-core/src/components/grid/stores/datasources/table.js index 847dfd2c6b..4f7f6a29f7 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/table.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/table.js @@ -10,7 +10,10 @@ export const createActions = context => { } const saveRow = async row => { - row.tableId = get(datasource)?.tableId + row = { + ...row, + tableId: get(datasource)?.tableId, + } return await API.saveRow(row, SuppressErrors) } diff --git a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js index ed31d0ae44..2a1ccddc43 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js @@ -11,8 +11,11 @@ export const createActions = context => { const saveRow = async row => { const $datasource = get(datasource) - row.tableId = $datasource?.tableId - row._viewId = $datasource?.id + row = { + ...row, + tableId: $datasource?.tableId, + _viewId: $datasource?.id, + } return { ...(await API.saveRow(row, SuppressErrors)), _viewId: row._viewId, diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index e4b69819ac..f23ce1e1b3 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -4,6 +4,7 @@ import { NewRowID, RowPageSize } from "../lib/constants" import { getCellID, parseCellID } from "../lib/utils" import { tick } from "svelte" import { Helpers } from "@budibase/bbui" +import { sleep } from "../../../utils/utils" export const createStores = () => { const rows = writable([]) @@ -274,11 +275,9 @@ export const createActions = context => { } // Adds a new row - const addRow = async (row, idx, bubble = false) => { + const addRow = async ({ row, idx, bubble = false, notify = true }) => { try { - // Create row. Spread row so we can mutate and enrich safely. - let newRow = { ...row } - newRow = await datasource.actions.addRow(newRow) + const newRow = await datasource.actions.addRow(row) // Update state if (idx != null) { @@ -291,8 +290,9 @@ export const createActions = context => { handleNewRows([newRow]) } - // Refresh row to ensure data is in the correct format - get(notifications).success("Row created successfully") + if (notify) { + get(notifications).success("Row created successfully") + } return newRow } catch (error) { if (bubble) { @@ -305,17 +305,72 @@ export const createActions = context => { // Duplicates a row, inserting the duplicate row after the existing one const duplicateRow = async row => { - let clone = { ...row } + let clone = cleanRow(row) delete clone._id delete clone._rev - delete clone.__idx try { - return await addRow(clone, row.__idx + 1, true) + const duped = await addRow({ + row: clone, + idx: row.__idx + 1, + bubble: true, + notify: false, + }) + get(notifications).success("Duplicated 1 row") + return duped } catch (error) { handleValidationError(row._id, error) } } + // Duplicates multiple rows, inserting them after the last source row + const bulkDuplicate = async rowsToDupe => { + // Find index of last row + const $rowLookupMap = get(rowLookupMap) + const index = Math.max(...rowsToDupe.map(row => $rowLookupMap[row._id])) + + // Clone and clean rows + const clones = rowsToDupe.map(row => { + let clone = cleanRow(row) + delete clone._id + delete clone._rev + return clone + }) + + // Create rows + let saved = [] + let failed = 0 + for (let clone of clones) { + try { + saved.push(await datasource.actions.addRow(clone)) + rowCacheMap[saved._id] = true + await sleep(50) // Small sleep to ensure we avoid rate limiting + } catch (error) { + failed++ + console.error("Duplicating row failed", error) + } + } + + // Add to state + if (saved.length) { + rows.update(state => { + return state.toSpliced(index + 1, 0, ...saved) + }) + } + + // Notify user + if (saved.length) { + get(notifications).success( + `Duplicated ${saved.length} row${saved.length === 1 ? "" : "s"}` + ) + } + if (failed) { + get(notifications).error( + `Failed to duplicate ${failed} row${failed === 1 ? "" : "s"}` + ) + } + return saved + } + // Replaces a row in state with the newly defined row, handling updates, // addition and deletion const replaceRow = (id, row) => { @@ -541,6 +596,7 @@ export const createActions = context => { actions: { addRow, duplicateRow, + bulkDuplicate, getRow, updateValue, applyRowChanges, diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index 1bee3d6c04..b8cfa5ad37 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -1,6 +1,8 @@ import { makePropSafe as safe } from "@budibase/string-templates" import { Helpers } from "@budibase/bbui" +export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + /** * Utility to wrap an async function and ensure all invocations happen * sequentially.