From 9657781df685cb70becc92be5adb6fb468eabccf Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 21 Jun 2024 20:38:48 +0100 Subject: [PATCH] Add multi to multi pasting --- .../src/components/grid/stores/clipboard.js | 131 ++++++++++++++---- .../src/components/grid/stores/selection.js | 2 +- 2 files changed, 107 insertions(+), 26 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/clipboard.js b/packages/frontend-core/src/components/grid/stores/clipboard.js index 25272d61b0..17b39c134b 100644 --- a/packages/frontend-core/src/components/grid/stores/clipboard.js +++ b/packages/frontend-core/src/components/grid/stores/clipboard.js @@ -5,9 +5,8 @@ import { parseCellID } from "../lib/utils" export const createStores = () => { const clipboard = writable({ value: null, - multiCellMode: false, + multiCellCopy: false, }) - return { clipboard, } @@ -16,10 +15,12 @@ export const createStores = () => { export const deriveStores = context => { const { clipboard, focusedCellAPI, selectedCellCount } = context + // Derive whether or not we're able to copy const copyAllowed = derived(focusedCellAPI, $focusedCellAPI => { return $focusedCellAPI != null }) + // Derive whether or not we're able to paste const pasteAllowed = derived( [clipboard, focusedCellAPI, selectedCellCount], ([$clipboard, $focusedCellAPI, $selectedCellCount]) => { @@ -30,7 +31,7 @@ export const deriveStores = context => { // this cell is readonly const multiCellPaste = $selectedCellCount > 1 if ( - !$clipboard.multiCellMode && + !$clipboard.multiCellCopy && !multiCellPaste && $focusedCellAPI.isReadonly() ) { @@ -49,44 +50,93 @@ export const deriveStores = context => { export const createActions = context => { const { clipboard, - selectedCellCount, focusedCellAPI, copyAllowed, pasteAllowed, - rows, selectedCells, + rowLookupMap, + rowChangeCache, + rows, + columnLookupMap, } = context + // Copies the currently selected value (or values) const copy = () => { if (!get(copyAllowed)) { return } - const $selectedCellCount = get(selectedCellCount) + const cellIds = Object.keys(get(selectedCells)) const $focusedCellAPI = get(focusedCellAPI) - const multiCellMode = $selectedCellCount > 1 + const multiCellCopy = cellIds.length > 1 // Multiple values to copy - if (multiCellMode) { - // TODO - return - } + if (multiCellCopy) { + const $rowLookupMap = get(rowLookupMap) + const $rowChangeCache = get(rowChangeCache) + const $rows = get(rows) + const $columnLookupMap = get(columnLookupMap) - // Single value to copy - const value = $focusedCellAPI.getValue() - clipboard.set({ - value, - multiCellMode, - }) + // Go through each selected cell and group all selected cell values by + // 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, + }) + } - // Also copy a stringified version to the clipboard - let stringified = "" - if (value != null && value !== "") { - // Only conditionally stringify to avoid redundant quotes around text - stringified = typeof value === "object" ? JSON.stringify(value) : value + // Sort rows by order + let value = [] + const sortedRowValues = Object.values(map) + .toSorted((a, b) => a.order - b.order) + .map(x => x.values) + + // Sort all values in each row by order + for (let rowValues of sortedRowValues) { + value.push( + rowValues.toSorted((a, b) => a.order - b.order).map(x => x.value) + ) + } + + // Update state + clipboard.set({ + value, + multiCellCopy: true, + }) + } else { + // Single value to copy + const value = $focusedCellAPI.getValue() + clipboard.set({ + value, + multiCellCopy, + }) + + // Also copy a stringified version to the clipboard + let stringified = "" + if (value != null && value !== "") { + // Only conditionally stringify to avoid redundant quotes around text + stringified = typeof value === "object" ? JSON.stringify(value) : value + } + Helpers.copyToClipboard(stringified) } - Helpers.copyToClipboard(stringified) } + // Pastes the previously copied value(s) into the selected cell(s) const paste = async () => { if (!get(pasteAllowed)) { return @@ -98,14 +148,45 @@ export const createActions = context => { } // Check if we're pasting into one or more cells - const cellIds = Object.keys(get(selectedCells)) + const $selectedCells = get(selectedCells) + const cellIds = Object.keys($selectedCells) const multiCellPaste = cellIds.length > 1 - if ($clipboard.multiCellMode) { + if ($clipboard.multiCellCopy) { if (multiCellPaste) { // Multi to multi (only paste selected cells) + const value = $clipboard.value + + // Find the top left index so we can find the relative offset for each + // 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 = {} + const $rowLookupMap = get(rowLookupMap) + const $columnLookupMap = get(columnLookupMap) + for (let cellId of cellIds) { + const { id, field } = parseCellID(cellId) + const rowIdx = $rowLookupMap[id] - minRowIdx + const colIdx = $columnLookupMap[field] - minColIdx + if (colIdx in (value[rowIdx] || [])) { + if (!changeMap[id]) { + changeMap[id] = {} + } + changeMap[id][field] = value[rowIdx][colIdx] + } + } + await rows.actions.bulkUpdate(changeMap) } else { // Multi to single (expand to paste all values) + // TODO } } else { if (multiCellPaste) { diff --git a/packages/frontend-core/src/components/grid/stores/selection.js b/packages/frontend-core/src/components/grid/stores/selection.js index ba1b1b5d32..bcbdd50079 100644 --- a/packages/frontend-core/src/components/grid/stores/selection.js +++ b/packages/frontend-core/src/components/grid/stores/selection.js @@ -60,7 +60,7 @@ export const deriveStores = context => { rowId = $rows[rowIdx]._id colName = $allVisibleColumns[colIdx].name cellId = getCellID(rowId, colName) - map[cellId] = true + map[cellId] = { rowIdx, colIdx } } } return map