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
|
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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -32,9 +32,9 @@ const DependencyOrderedStores = [
|
||||||
NonPlus,
|
NonPlus,
|
||||||
Datasource,
|
Datasource,
|
||||||
Columns,
|
Columns,
|
||||||
|
Validation,
|
||||||
Rows,
|
Rows,
|
||||||
UI,
|
UI,
|
||||||
Validation,
|
|
||||||
Resize,
|
Resize,
|
||||||
Viewport,
|
Viewport,
|
||||||
Reorder,
|
Reorder,
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue