1
0
Fork 0
mirror of synced 2024-09-17 17:57:47 +12:00

Add support for bulk pasting a single value into multiple cells

This commit is contained in:
Andrew Kingston 2024-06-21 16:30:51 +01:00
parent 502c2541e5
commit ad0d300ff9
No known key found for this signature in database
5 changed files with 152 additions and 41 deletions

View file

@ -34,8 +34,10 @@
let api
// Get the error for this cell if the row is focused
$: error = getErrorStore(rowFocused, cellId)
$: cellSelected = selectedCells[cellId]
// Get the error for this cell if the cell is focused or selected
$: error = getErrorStore(rowFocused || cellSelected, cellId)
// Determine if the cell is editable
$: readonly =
@ -53,7 +55,6 @@
}
// Callbacks for cell selection
$: cellSelected = selectedCells[cellId]
$: updateSelectionCallback = isSelectingCells ? updateSelection : null
$: stopSelectionCallback = isSelectingCells ? stopSelection : null

View file

@ -1,5 +1,6 @@
import { derived, writable, get } from "svelte/store"
import { Helpers } from "@budibase/bbui"
import { parseCellID } from "../lib/utils"
export const createStores = () => {
const clipboard = writable({
@ -52,6 +53,8 @@ export const createActions = context => {
focusedCellAPI,
copyAllowed,
pasteAllowed,
rows,
selectedCells,
} = context
const copy = () => {
@ -84,7 +87,7 @@ export const createActions = context => {
Helpers.copyToClipboard(stringified)
}
const paste = () => {
const paste = async () => {
if (!get(pasteAllowed)) {
return
}
@ -95,8 +98,8 @@ export const createActions = context => {
}
// Check if we're pasting into one or more cells
const $selectedCellCount = get(selectedCellCount)
const multiCellPaste = $selectedCellCount > 1
const cellIds = Object.keys(get(selectedCells))
const multiCellPaste = cellIds.length > 1
if ($clipboard.multiCellMode) {
if (multiCellPaste) {
@ -107,6 +110,15 @@ export const createActions = context => {
} else {
if (multiCellPaste) {
// Single to multi (duplicate value in all selected cells)
let changeMap = {}
for (let cellId of cellIds) {
const { id, field } = parseCellID(cellId)
if (!changeMap[id]) {
changeMap[id] = {}
}
changeMap[id][field] = $clipboard.value
}
await rows.actions.bulkUpdate(changeMap)
} else {
// Single to single
$focusedCellAPI.setValue($clipboard.value)

View file

@ -32,9 +32,9 @@ const DependencyOrderedStores = [
NonPlus,
Datasource,
Columns,
Validation,
Rows,
UI,
Validation,
Resize,
Viewport,
Reorder,

View file

@ -264,11 +264,6 @@ export const createActions = context => {
for (let column of missingColumns) {
get(notifications).error(`${column} is required but is missing`)
}
// Focus the first cell with an error
if (erroredColumns.length) {
focusedCellId.set(getCellID(rowId, erroredColumns[0]))
}
} else {
get(notifications).error(errorString || "An unknown error occurred")
}
@ -299,6 +294,7 @@ export const createActions = context => {
throw error
} else {
handleValidationError(NewRowID, error)
validation.actions.focusFirstRowError(NewRowID)
}
}
}
@ -319,6 +315,7 @@ export const createActions = context => {
return duped
} catch (error) {
handleValidationError(row._id, error)
validation.actions.focusFirstRowError(row._id)
}
}
@ -447,8 +444,14 @@ export const createActions = context => {
return true
}
// Saves any pending changes to a row
const applyRowChanges = async rowId => {
// Saves any pending changes to a row, as well as any additional changes
// specified
const applyRowChanges = async ({
rowId,
changes = null,
updateState = true,
handleErrors = true,
}) => {
const $rows = get(rows)
const $rowLookupMap = get(rowLookupMap)
const index = $rowLookupMap[rowId]
@ -456,6 +459,7 @@ export const createActions = context => {
if (row == null) {
return
}
let savedRow
// Save change
try {
@ -466,19 +470,24 @@ export const createActions = context => {
}))
// Update row
const changes = get(rowChangeCache)[rowId]
const newRow = { ...cleanRow(row), ...changes }
const saved = await datasource.actions.updateRow(newRow)
const newRow = {
...cleanRow(row),
...get(rowChangeCache)[rowId],
...changes,
}
savedRow = await datasource.actions.updateRow(newRow)
// Update row state after a successful change
if (saved?._id) {
rows.update(state => {
state[index] = saved
return state.slice()
})
} else if (saved?.id) {
if (savedRow?._id) {
if (updateState) {
rows.update(state => {
state[index] = savedRow
return state.slice()
})
}
} else if (savedRow?.id) {
// Handle users table edge case
await refreshRow(saved.id)
await refreshRow(savedRow.id)
}
// Wipe row change cache for any values which have been saved
@ -492,7 +501,10 @@ export const createActions = context => {
return state
})
} catch (error) {
handleValidationError(rowId, error)
if (handleErrors) {
handleValidationError(rowId, error)
validation.actions.focusFirstRowError(rowId)
}
}
// Decrement change count for this row
@ -500,6 +512,7 @@ export const createActions = context => {
...state,
[rowId]: (state[rowId] || 1) - 1,
}))
return savedRow
}
// Updates a value of a row
@ -510,6 +523,63 @@ export const createActions = context => {
}
}
const bulkUpdate = async changeMap => {
const rowIds = Object.keys(changeMap || {})
if (!rowIds.length) {
return
}
// Update rows
let updated = []
let failed = 0
for (let rowId of rowIds) {
if (!Object.keys(changeMap[rowId] || {}).length) {
continue
}
try {
const updatedRow = await applyRowChanges({
rowId,
changes: changeMap[rowId],
updateState: false,
handleErrors: false,
})
if (updatedRow) {
updated.push(updatedRow)
} else {
failed++
}
await sleep(50) // Small sleep to ensure we avoid rate limiting
} catch (error) {
failed++
console.error("Failed to update row", error)
}
}
// Update state
if (updated.length) {
const $rowLookupMap = get(rowLookupMap)
rows.update(state => {
for (let row of updated) {
const index = $rowLookupMap[row._id]
state[index] = row
}
return state.slice()
})
}
// Notify user
if (updated.length) {
get(notifications).success(
`Updated ${updated.length} row${updated.length === 1 ? "" : "s"}`
)
}
if (failed) {
get(notifications).error(
`Failed to update ${failed} row${failed === 1 ? "" : "s"}`
)
}
}
// Deletes an array of rows
const deleteRows = async rowsToDelete => {
if (!rowsToDelete?.length) {
@ -607,6 +677,7 @@ export const createActions = context => {
replaceRow,
refreshData,
cleanRow,
bulkUpdate,
},
},
}

View file

@ -1,5 +1,5 @@
import { writable, get, derived } from "svelte/store"
import { getCellID, parseCellID } from "../lib/utils"
import { parseCellID } from "../lib/utils"
// Normally we would break out actions into the explicit "createActions"
// function, but for validation all these actions are pure so can go into
@ -7,18 +7,38 @@ import { getCellID, parseCellID } from "../lib/utils"
export const createStores = () => {
const validation = writable({})
return {
validation,
}
}
export const deriveStores = context => {
const { validation } = context
// Derive which rows have errors so that we can use that info later
const rowErrorMap = derived(validation, $validation => {
const validationRowLookupMap = derived(validation, $validation => {
let map = {}
Object.entries($validation).forEach(([key, error]) => {
// Extract row ID from all errored cell IDs
if (error) {
map[parseCellID(key).id] = true
const rowId = parseCellID(key).id
if (!map[rowId]) {
map[rowId] = []
}
map[rowId].push(key)
}
})
return map
})
return {
validationRowLookupMap,
}
}
export const createActions = context => {
const { validation, focusedCellId, validationRowLookupMap } = context
const setError = (cellId, error) => {
if (!cellId) {
return
@ -30,7 +50,15 @@ export const createStores = () => {
}
const rowHasErrors = rowId => {
return get(rowErrorMap)[rowId]
return get(validationRowLookupMap)[rowId]?.length > 0
}
const focusFirstRowError = rowId => {
const errorCells = get(validationRowLookupMap)[rowId]
const cellId = errorCells?.[0]
if (cellId) {
focusedCellId.set(cellId)
}
}
return {
@ -39,28 +67,27 @@ export const createStores = () => {
actions: {
setError,
rowHasErrors,
focusFirstRowError,
},
},
}
}
export const initialise = context => {
const { validation, previousFocusedRowId, columns, stickyColumn } = context
const { validation, previousFocusedRowId, validationRowLookupMap } = context
// Remove validation errors from previous focused row
// Remove validation errors when changing rows
previousFocusedRowId.subscribe(id => {
if (id) {
const $columns = get(columns)
const $stickyColumn = get(stickyColumn)
validation.update(state => {
$columns.forEach(column => {
state[getCellID(id, column.name)] = null
const errorCells = get(validationRowLookupMap)[id]
if (errorCells?.length) {
validation.update(state => {
for (let cellId of errorCells) {
delete state[cellId]
}
return state
})
if ($stickyColumn) {
state[getCellID(id, stickyColumn.name)] = null
}
return state
})
}
}
})
}