1
0
Fork 0
mirror of synced 2024-09-19 10:48:30 +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 let api
// Get the error for this cell if the row is focused $: cellSelected = selectedCells[cellId]
$: error = getErrorStore(rowFocused, 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 // Determine if the cell is editable
$: readonly = $: readonly =
@ -53,7 +55,6 @@
} }
// Callbacks for cell selection // Callbacks for cell selection
$: cellSelected = selectedCells[cellId]
$: updateSelectionCallback = isSelectingCells ? updateSelection : null $: updateSelectionCallback = isSelectingCells ? updateSelection : null
$: stopSelectionCallback = isSelectingCells ? stopSelection : null $: stopSelectionCallback = isSelectingCells ? stopSelection : null

View file

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

View file

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

View file

@ -264,11 +264,6 @@ export const createActions = context => {
for (let column of missingColumns) { for (let column of missingColumns) {
get(notifications).error(`${column} is required but is missing`) 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 { } else {
get(notifications).error(errorString || "An unknown error occurred") get(notifications).error(errorString || "An unknown error occurred")
} }
@ -299,6 +294,7 @@ export const createActions = context => {
throw error throw error
} else { } else {
handleValidationError(NewRowID, error) handleValidationError(NewRowID, error)
validation.actions.focusFirstRowError(NewRowID)
} }
} }
} }
@ -319,6 +315,7 @@ export const createActions = context => {
return duped return duped
} catch (error) { } catch (error) {
handleValidationError(row._id, error) handleValidationError(row._id, error)
validation.actions.focusFirstRowError(row._id)
} }
} }
@ -447,8 +444,14 @@ export const createActions = context => {
return true return true
} }
// Saves any pending changes to a row // Saves any pending changes to a row, as well as any additional changes
const applyRowChanges = async rowId => { // specified
const applyRowChanges = async ({
rowId,
changes = null,
updateState = true,
handleErrors = true,
}) => {
const $rows = get(rows) const $rows = get(rows)
const $rowLookupMap = get(rowLookupMap) const $rowLookupMap = get(rowLookupMap)
const index = $rowLookupMap[rowId] const index = $rowLookupMap[rowId]
@ -456,6 +459,7 @@ export const createActions = context => {
if (row == null) { if (row == null) {
return return
} }
let savedRow
// Save change // Save change
try { try {
@ -466,19 +470,24 @@ export const createActions = context => {
})) }))
// Update row // Update row
const changes = get(rowChangeCache)[rowId] const newRow = {
const newRow = { ...cleanRow(row), ...changes } ...cleanRow(row),
const saved = await datasource.actions.updateRow(newRow) ...get(rowChangeCache)[rowId],
...changes,
}
savedRow = await datasource.actions.updateRow(newRow)
// Update row state after a successful change // Update row state after a successful change
if (saved?._id) { if (savedRow?._id) {
rows.update(state => { if (updateState) {
state[index] = saved rows.update(state => {
return state.slice() state[index] = savedRow
}) return state.slice()
} else if (saved?.id) { })
}
} else if (savedRow?.id) {
// Handle users table edge case // Handle users table edge case
await refreshRow(saved.id) await refreshRow(savedRow.id)
} }
// Wipe row change cache for any values which have been saved // Wipe row change cache for any values which have been saved
@ -492,7 +501,10 @@ export const createActions = context => {
return state return state
}) })
} catch (error) { } catch (error) {
handleValidationError(rowId, error) if (handleErrors) {
handleValidationError(rowId, error)
validation.actions.focusFirstRowError(rowId)
}
} }
// Decrement change count for this row // Decrement change count for this row
@ -500,6 +512,7 @@ export const createActions = context => {
...state, ...state,
[rowId]: (state[rowId] || 1) - 1, [rowId]: (state[rowId] || 1) - 1,
})) }))
return savedRow
} }
// Updates a value of a row // 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 // Deletes an array of rows
const deleteRows = async rowsToDelete => { const deleteRows = async rowsToDelete => {
if (!rowsToDelete?.length) { if (!rowsToDelete?.length) {
@ -607,6 +677,7 @@ export const createActions = context => {
replaceRow, replaceRow,
refreshData, refreshData,
cleanRow, cleanRow,
bulkUpdate,
}, },
}, },
} }

View file

@ -1,5 +1,5 @@
import { writable, get, derived } from "svelte/store" 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" // Normally we would break out actions into the explicit "createActions"
// function, but for validation all these actions are pure so can go into // 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 = () => { export const createStores = () => {
const validation = writable({}) 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 // 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 = {} let map = {}
Object.entries($validation).forEach(([key, error]) => { Object.entries($validation).forEach(([key, error]) => {
// Extract row ID from all errored cell IDs // Extract row ID from all errored cell IDs
if (error) { if (error) {
map[parseCellID(key).id] = true const rowId = parseCellID(key).id
if (!map[rowId]) {
map[rowId] = []
}
map[rowId].push(key)
} }
}) })
return map return map
}) })
return {
validationRowLookupMap,
}
}
export const createActions = context => {
const { validation, focusedCellId, validationRowLookupMap } = context
const setError = (cellId, error) => { const setError = (cellId, error) => {
if (!cellId) { if (!cellId) {
return return
@ -30,7 +50,15 @@ export const createStores = () => {
} }
const rowHasErrors = rowId => { 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 { return {
@ -39,28 +67,27 @@ export const createStores = () => {
actions: { actions: {
setError, setError,
rowHasErrors, rowHasErrors,
focusFirstRowError,
}, },
}, },
} }
} }
export const initialise = context => { 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 => { previousFocusedRowId.subscribe(id => {
if (id) { if (id) {
const $columns = get(columns) const errorCells = get(validationRowLookupMap)[id]
const $stickyColumn = get(stickyColumn) if (errorCells?.length) {
validation.update(state => { validation.update(state => {
$columns.forEach(column => { for (let cellId of errorCells) {
state[getCellID(id, column.name)] = null delete state[cellId]
}
return state
}) })
if ($stickyColumn) { }
state[getCellID(id, stickyColumn.name)] = null
}
return state
})
} }
}) })
} }