Simplify new paste logic

This commit is contained in:
Andrew Kingston 2024-06-22 19:28:52 +01:00
parent b094f0bc31
commit d4d63c6115
No known key found for this signature in database
9 changed files with 98 additions and 114 deletions

View File

@ -86,17 +86,18 @@
if (e.button !== 0) { if (e.button !== 0) {
return return
} }
// focusedCellId.set(cellId) selectedCells.actions.startSelecting(cellId)
selectedCells.actions.start(cellId)
} }
const updateSelection = e => { const updateSelection = () => {
focusedCellId.set(null) if ($focusedCellId) {
selectedCells.actions.update(cellId) focusedCellId.set(null)
}
selectedCells.actions.updateTarget(cellId)
} }
const stopSelection = e => { const stopSelection = () => {
selectedCells.actions.stop() selectedCells.actions.stopSelecting()
} }
</script> </script>

View File

@ -40,6 +40,6 @@
onConfirm={clipboard.actions.paste} onConfirm={clipboard.actions.paste}
size="M" size="M"
> >
Are you sure you want to paste values into {$selectedCellCount} cells? Are you sure you want to paste? This will update multiple values.
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -12,15 +12,15 @@
selectedRows, selectedRows,
visibleColumns, visibleColumns,
hoveredRowId, hoveredRowId,
selectedCellMap,
focusedRow, focusedRow,
contentLines, contentLines,
isDragging, isDragging,
dispatch, dispatch,
rows, rows,
columnRenderMap, columnRenderMap,
userCellMap,
isSelectingCells, isSelectingCells,
selectedCells, selectedCellMap,
selectedCellCount, selectedCellCount,
} = getContext("grid") } = getContext("grid")
@ -48,12 +48,12 @@
{row} {row}
{rowFocused} {rowFocused}
{rowSelected} {rowSelected}
cellSelected={$selectedCells[cellId]} cellSelected={$selectedCellMap[cellId]}
highlighted={rowHovered || rowFocused || reorderSource === column.name} highlighted={rowHovered || rowFocused || reorderSource === column.name}
rowIdx={row.__idx} rowIdx={row.__idx}
topRow={top} topRow={top}
focused={$focusedCellId === cellId} focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]} selectedUser={$userCellMap[cellId]}
width={column.width} width={column.width}
contentLines={$contentLines} contentLines={$contentLines}
hidden={!$columnRenderMap[column.name]} hidden={!$columnRenderMap[column.name]}

View File

@ -19,6 +19,7 @@
hoveredRowId, hoveredRowId,
config, config,
selectedCellMap, selectedCellMap,
userCellMap,
focusedRow, focusedRow,
scrollLeft, scrollLeft,
dispatch, dispatch,
@ -91,12 +92,12 @@
{cellId} {cellId}
{rowFocused} {rowFocused}
{rowSelected} {rowSelected}
cellSelected={$selectedCells[cellId]} cellSelected={$selectedCellMap[cellId]}
highlighted={rowHovered || rowFocused} highlighted={rowHovered || rowFocused}
rowIdx={row.__idx} rowIdx={row.__idx}
topRow={idx === 0} topRow={idx === 0}
focused={$focusedCellId === cellId} focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]} selectedUser={$userCellMap[cellId]}
width={$stickyColumn.width} width={$stickyColumn.width}
column={$stickyColumn} column={$stickyColumn}
contentLines={$contentLines} contentLines={$contentLines}

View File

@ -13,22 +13,25 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { clipboard, focusedCellAPI, selectedCellCount } = context const { clipboard, focusedCellAPI, selectedCellCount, config } = context
// Derive whether or not we're able to copy // Derive whether or not we're able to copy
const copyAllowed = derived(focusedCellAPI, $focusedCellAPI => { const copyAllowed = derived(
return $focusedCellAPI != null [focusedCellAPI, selectedCellCount],
}) ([$focusedCellAPI, $selectedCellCount]) => {
return $focusedCellAPI || $selectedCellCount
}
)
// Derive whether or not we're able to paste // Derive whether or not we're able to paste
const pasteAllowed = derived( const pasteAllowed = derived(
[clipboard, focusedCellAPI, selectedCellCount], [clipboard, focusedCellAPI, selectedCellCount, config],
([$clipboard, $focusedCellAPI, $selectedCellCount]) => { ([$clipboard, $focusedCellAPI, $selectedCellCount, $config]) => {
if ($clipboard.value == null || !$focusedCellAPI) { if ($clipboard.value == null || !$config.canEditRows) {
return false return false
} }
// Prevent pasting into a single cell, if we have a single cell value and
// this cell is readonly // Prevent single-single pasting if the cell is readonly
const multiCellPaste = $selectedCellCount > 1 const multiCellPaste = $selectedCellCount > 1
if ( if (
!$clipboard.multiCellCopy && !$clipboard.multiCellCopy &&
@ -37,7 +40,8 @@ export const deriveStores = context => {
) { ) {
return false return false
} }
return true
return $focusedCellAPI || $selectedCellCount
} }
) )
@ -54,10 +58,10 @@ export const createActions = context => {
copyAllowed, copyAllowed,
pasteAllowed, pasteAllowed,
selectedCells, selectedCells,
selectedCellCount,
rowLookupMap, rowLookupMap,
rowChangeCache, rowChangeCache,
rows, rows,
columnLookupMap,
} = context } = context
// Copies the currently selected value (or values) // Copies the currently selected value (or values)
@ -65,52 +69,31 @@ export const createActions = context => {
if (!get(copyAllowed)) { if (!get(copyAllowed)) {
return return
} }
const cellIds = Object.keys(get(selectedCells)) const $selectedCells = get(selectedCells)
const $focusedCellAPI = get(focusedCellAPI) const $focusedCellAPI = get(focusedCellAPI)
const multiCellCopy = cellIds.length > 1 const $selectedCellCount = get(selectedCellCount)
const multiCellCopy = $selectedCellCount > 1
// Multiple values to copy // Multiple values to copy
if (multiCellCopy) { if (multiCellCopy) {
const $rowLookupMap = get(rowLookupMap) const $rowLookupMap = get(rowLookupMap)
const $rowChangeCache = get(rowChangeCache) const $rowChangeCache = get(rowChangeCache)
const $rows = get(rows) const $rows = get(rows)
const $columnLookupMap = get(columnLookupMap)
// Go through each selected cell and group all selected cell values by // Extract value of each selected cell
// their row ID. Order is important for pasting, so we store the index of
// both rows and values.
let map = {}
for (let cellId of cellIds) {
const { id, field } = parseCellID(cellId)
const index = $rowLookupMap[id]
if (!map[id]) {
map[id] = {
order: index,
values: [],
}
}
const row = {
...$rows[index],
...$rowChangeCache[id],
}
const columnIndex = $columnLookupMap[field]
map[id].values.push({
value: row[field],
order: columnIndex,
})
}
// Sort rows by order
let value = [] let value = []
const sortedRowValues = Object.values(map) for (let row of $selectedCells) {
.toSorted((a, b) => a.order - b.order) const rowValues = []
.map(x => x.values) for (let cellId of row) {
const { id, field } = parseCellID(cellId)
// Sort all values in each row by order const rowIndex = $rowLookupMap[id]
for (let rowValues of sortedRowValues) { const row = {
value.push( ...$rows[rowIndex],
rowValues.toSorted((a, b) => a.order - b.order).map(x => x.value) ...$rowChangeCache[id],
) }
rowValues.push(row[field])
}
value.push(rowValues)
} }
// Update state // Update state
@ -141,42 +124,26 @@ export const createActions = context => {
if (!get(pasteAllowed)) { if (!get(pasteAllowed)) {
return return
} }
const $clipboard = get(clipboard) const { value, multiCellCopy } = get(clipboard)
const $focusedCellAPI = get(focusedCellAPI) const $focusedCellAPI = get(focusedCellAPI)
if ($clipboard.value == null || !$focusedCellAPI) {
return
}
// Check if we're pasting into one or more cells
const $selectedCells = get(selectedCells) const $selectedCells = get(selectedCells)
const cellIds = Object.keys($selectedCells) const $selectedCellCount = get(selectedCellCount)
const multiCellPaste = cellIds.length > 1 const multiCellPaste = $selectedCellCount > 1
if ($clipboard.multiCellCopy) { // Choose paste strategy
if (multiCellCopy) {
if (multiCellPaste) { if (multiCellPaste) {
// Multi to multi (only paste selected cells) // Multi to multi (only paste selected cells)
const value = $clipboard.value // Find the extent at which we can paste
const rowExtent = Math.min(value.length, $selectedCells.length)
const colExtent = Math.min(value[0].length, $selectedCells[0].length)
// Find the top left index so we can find the relative offset for each // Build change map
// cell
let rowIndices = []
let columnIndices = []
for (let cellId of cellIds) {
rowIndices.push($selectedCells[cellId].rowIdx)
columnIndices.push($selectedCells[cellId].colIdx)
}
const minRowIdx = Math.min(...rowIndices)
const minColIdx = Math.min(...columnIndices)
// Build change map of values to patch
let changeMap = {} let changeMap = {}
const $rowLookupMap = get(rowLookupMap) for (let rowIdx = 0; rowIdx < rowExtent; rowIdx++) {
const $columnLookupMap = get(columnLookupMap) for (let colIdx = 0; colIdx < colExtent; colIdx++) {
for (let cellId of cellIds) { const cellId = $selectedCells[rowIdx][colIdx]
const { id, field } = parseCellID(cellId) const { id, field } = parseCellID(cellId)
const rowIdx = $rowLookupMap[id] - minRowIdx
const colIdx = $columnLookupMap[field] - minColIdx
if (colIdx in (value[rowIdx] || [])) {
if (!changeMap[id]) { if (!changeMap[id]) {
changeMap[id] = {} changeMap[id] = {}
} }
@ -192,17 +159,19 @@ export const createActions = context => {
if (multiCellPaste) { if (multiCellPaste) {
// Single to multi (duplicate value in all selected cells) // Single to multi (duplicate value in all selected cells)
let changeMap = {} let changeMap = {}
for (let cellId of cellIds) { for (let row of $selectedCells) {
const { id, field } = parseCellID(cellId) for (let cellId of row) {
if (!changeMap[id]) { const { id, field } = parseCellID(cellId)
changeMap[id] = {} if (!changeMap[id]) {
changeMap[id] = {}
}
changeMap[id][field] = value
} }
changeMap[id][field] = $clipboard.value
} }
await rows.actions.bulkUpdate(changeMap) await rows.actions.bulkUpdate(changeMap)
} else { } else {
// Single to single // Single to single
$focusedCellAPI.setValue($clipboard.value) $focusedCellAPI.setValue(value)
} }
} }
} }

View File

@ -40,8 +40,8 @@ const DependencyOrderedStores = [
Users, Users,
Menu, Menu,
Pagination, Pagination,
Clipboard,
Config, Config,
Clipboard,
Notifications, Notifications,
Cache, Cache,
] ]

View File

@ -21,7 +21,7 @@ export const createActions = context => {
gridID, gridID,
selectedRows, selectedRows,
selectedRowCount, selectedRowCount,
selectedCells, selectedCellMap,
selectedCellCount, selectedCellCount,
} = context } = context
@ -52,7 +52,7 @@ export const createActions = context => {
// Check if there are multiple cells selected, and if this is one of them // Check if there are multiple cells selected, and if this is one of them
let multiCellMode = false let multiCellMode = false
if (!multiRowMode && get(selectedCellCount) > 1) { if (!multiRowMode && get(selectedCellCount) > 1) {
if (get(selectedCells)[cellId]) { if (get(selectedCellMap)[cellId]) {
multiCellMode = true multiCellMode = true
} }
} }

View File

@ -109,7 +109,7 @@ export const deriveStores = context => {
([$cellSelection, $rowLookupMap, $columnLookupMap]) => { ([$cellSelection, $rowLookupMap, $columnLookupMap]) => {
const { sourceCellId, targetCellId } = $cellSelection const { sourceCellId, targetCellId } = $cellSelection
if (!sourceCellId || !targetCellId || sourceCellId === targetCellId) { if (!sourceCellId || !targetCellId || sourceCellId === targetCellId) {
return {} return []
} }
const $rows = get(rows) const $rows = get(rows)
const $allVisibleColumns = get(allVisibleColumns) const $allVisibleColumns = get(allVisibleColumns)
@ -130,24 +130,36 @@ export const deriveStores = context => {
const lowerColIndex = Math.min(sourceColIndex, targetColIndex) const lowerColIndex = Math.min(sourceColIndex, targetColIndex)
const upperColIndex = Math.max(sourceColIndex, targetColIndex) const upperColIndex = Math.max(sourceColIndex, targetColIndex)
// Build map of all cells inside these bounds // Build 2 dimensional array of all cells inside these bounds
let map = {} let cells = []
let rowId, colName, cellId let rowId, colName
for (let rowIdx = lowerRowIndex; rowIdx <= upperRowIndex; rowIdx++) { for (let rowIdx = lowerRowIndex; rowIdx <= upperRowIndex; rowIdx++) {
let rowCells = []
for (let colIdx = lowerColIndex; colIdx <= upperColIndex; colIdx++) { for (let colIdx = lowerColIndex; colIdx <= upperColIndex; colIdx++) {
rowId = $rows[rowIdx]._id rowId = $rows[rowIdx]._id
colName = $allVisibleColumns[colIdx].name colName = $allVisibleColumns[colIdx].name
cellId = getCellID(rowId, colName) rowCells.push(getCellID(rowId, colName))
map[cellId] = { rowIdx, colIdx }
} }
cells.push(rowCells)
} }
return map return cells
} }
) )
// Derive a quick lookup map of the selected cells
const selectedCellMap = derived(selectedCells, $selectedCells => {
let map = {}
for (let row of $selectedCells) {
for (let cell of row) {
map[cell] = true
}
}
return map
})
// Derive the count of the selected cells // Derive the count of the selected cells
const selectedCellCount = derived(selectedCells, $selectedCells => { const selectedCellCount = derived(selectedCellMap, $selectedCellMap => {
return Object.keys($selectedCells).length return Object.keys($selectedCellMap).length
}) })
return { return {
@ -158,6 +170,7 @@ export const deriveStores = context => {
selectedRowCount, selectedRowCount,
isSelectingCells, isSelectingCells,
selectedCells, selectedCells,
selectedCellMap,
selectedCellCount, selectedCellCount,
} }
} }
@ -272,9 +285,9 @@ export const createActions = context => {
selectedCells: { selectedCells: {
...selectedCells, ...selectedCells,
actions: { actions: {
start: startCellSelection, startSelecting: startCellSelection,
update: updateCellSelection, updateTarget: updateCellSelection,
stop: stopCellSelection, stopSelecting: stopCellSelection,
clear: clearCellSelection, clear: clearCellSelection,
}, },
}, },

View File

@ -25,7 +25,7 @@ export const deriveStores = context => {
// Generate a lookup map of cell ID to the user that has it selected, to make // Generate a lookup map of cell ID to the user that has it selected, to make
// lookups inside cells extremely fast // lookups inside cells extremely fast
const selectedCellMap = derived( const userCellMap = derived(
[users, focusedCellId], [users, focusedCellId],
([$users, $focusedCellId]) => { ([$users, $focusedCellId]) => {
let map = {} let map = {}
@ -40,7 +40,7 @@ export const deriveStores = context => {
) )
return { return {
selectedCellMap, userCellMap,
} }
} }