Add support for bulk pasting a single value into multiple cells
This commit is contained in:
parent
502c2541e5
commit
ad0d300ff9
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -32,9 +32,9 @@ const DependencyOrderedStores = [
|
|||
NonPlus,
|
||||
Datasource,
|
||||
Columns,
|
||||
Validation,
|
||||
Rows,
|
||||
UI,
|
||||
Validation,
|
||||
Resize,
|
||||
Viewport,
|
||||
Reorder,
|
||||
|
|
|
@ -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) {
|
||||
if (savedRow?._id) {
|
||||
if (updateState) {
|
||||
rows.update(state => {
|
||||
state[index] = saved
|
||||
state[index] = savedRow
|
||||
return state.slice()
|
||||
})
|
||||
} else if (saved?.id) {
|
||||
}
|
||||
} 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) {
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
const errorCells = get(validationRowLookupMap)[id]
|
||||
if (errorCells?.length) {
|
||||
validation.update(state => {
|
||||
$columns.forEach(column => {
|
||||
state[getCellID(id, column.name)] = null
|
||||
})
|
||||
if ($stickyColumn) {
|
||||
state[getCellID(id, stickyColumn.name)] = null
|
||||
for (let cellId of errorCells) {
|
||||
delete state[cellId]
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue