diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index 800d612824..845f919e4a 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -86,17 +86,18 @@ if (e.button !== 0) { return } - // focusedCellId.set(cellId) - selectedCells.actions.start(cellId) + selectedCells.actions.startSelecting(cellId) } - const updateSelection = e => { - focusedCellId.set(null) - selectedCells.actions.update(cellId) + const updateSelection = () => { + if ($focusedCellId) { + focusedCellId.set(null) + } + selectedCells.actions.updateTarget(cellId) } - const stopSelection = e => { - selectedCells.actions.stop() + const stopSelection = () => { + selectedCells.actions.stopSelecting() } diff --git a/packages/frontend-core/src/components/grid/controls/ClipboardHandler.svelte b/packages/frontend-core/src/components/grid/controls/ClipboardHandler.svelte index 87b881e8ad..5ae120d47a 100644 --- a/packages/frontend-core/src/components/grid/controls/ClipboardHandler.svelte +++ b/packages/frontend-core/src/components/grid/controls/ClipboardHandler.svelte @@ -40,6 +40,6 @@ onConfirm={clipboard.actions.paste} 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. diff --git a/packages/frontend-core/src/components/grid/layout/GridRow.svelte b/packages/frontend-core/src/components/grid/layout/GridRow.svelte index a01d9de7df..c51d847d9c 100644 --- a/packages/frontend-core/src/components/grid/layout/GridRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/GridRow.svelte @@ -12,15 +12,15 @@ selectedRows, visibleColumns, hoveredRowId, - selectedCellMap, focusedRow, contentLines, isDragging, dispatch, rows, columnRenderMap, + userCellMap, isSelectingCells, - selectedCells, + selectedCellMap, selectedCellCount, } = getContext("grid") @@ -48,12 +48,12 @@ {row} {rowFocused} {rowSelected} - cellSelected={$selectedCells[cellId]} + cellSelected={$selectedCellMap[cellId]} highlighted={rowHovered || rowFocused || reorderSource === column.name} rowIdx={row.__idx} topRow={top} focused={$focusedCellId === cellId} - selectedUser={$selectedCellMap[cellId]} + selectedUser={$userCellMap[cellId]} width={column.width} contentLines={$contentLines} hidden={!$columnRenderMap[column.name]} diff --git a/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte b/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte index d9b3af9436..7fb9906635 100644 --- a/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte +++ b/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte @@ -19,6 +19,7 @@ hoveredRowId, config, selectedCellMap, + userCellMap, focusedRow, scrollLeft, dispatch, @@ -91,12 +92,12 @@ {cellId} {rowFocused} {rowSelected} - cellSelected={$selectedCells[cellId]} + cellSelected={$selectedCellMap[cellId]} highlighted={rowHovered || rowFocused} rowIdx={row.__idx} topRow={idx === 0} focused={$focusedCellId === cellId} - selectedUser={$selectedCellMap[cellId]} + selectedUser={$userCellMap[cellId]} width={$stickyColumn.width} column={$stickyColumn} contentLines={$contentLines} diff --git a/packages/frontend-core/src/components/grid/stores/clipboard.js b/packages/frontend-core/src/components/grid/stores/clipboard.js index 17b39c134b..638e9414ea 100644 --- a/packages/frontend-core/src/components/grid/stores/clipboard.js +++ b/packages/frontend-core/src/components/grid/stores/clipboard.js @@ -13,22 +13,25 @@ export const createStores = () => { } export const deriveStores = context => { - const { clipboard, focusedCellAPI, selectedCellCount } = context + const { clipboard, focusedCellAPI, selectedCellCount, config } = context // Derive whether or not we're able to copy - const copyAllowed = derived(focusedCellAPI, $focusedCellAPI => { - return $focusedCellAPI != null - }) + const copyAllowed = derived( + [focusedCellAPI, selectedCellCount], + ([$focusedCellAPI, $selectedCellCount]) => { + return $focusedCellAPI || $selectedCellCount + } + ) // Derive whether or not we're able to paste const pasteAllowed = derived( - [clipboard, focusedCellAPI, selectedCellCount], - ([$clipboard, $focusedCellAPI, $selectedCellCount]) => { - if ($clipboard.value == null || !$focusedCellAPI) { + [clipboard, focusedCellAPI, selectedCellCount, config], + ([$clipboard, $focusedCellAPI, $selectedCellCount, $config]) => { + if ($clipboard.value == null || !$config.canEditRows) { 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 if ( !$clipboard.multiCellCopy && @@ -37,7 +40,8 @@ export const deriveStores = context => { ) { return false } - return true + + return $focusedCellAPI || $selectedCellCount } ) @@ -54,10 +58,10 @@ export const createActions = context => { copyAllowed, pasteAllowed, selectedCells, + selectedCellCount, rowLookupMap, rowChangeCache, rows, - columnLookupMap, } = context // Copies the currently selected value (or values) @@ -65,52 +69,31 @@ export const createActions = context => { if (!get(copyAllowed)) { return } - const cellIds = Object.keys(get(selectedCells)) + const $selectedCells = get(selectedCells) const $focusedCellAPI = get(focusedCellAPI) - const multiCellCopy = cellIds.length > 1 + const $selectedCellCount = get(selectedCellCount) + const multiCellCopy = $selectedCellCount > 1 // Multiple values to copy if (multiCellCopy) { const $rowLookupMap = get(rowLookupMap) const $rowChangeCache = get(rowChangeCache) const $rows = get(rows) - const $columnLookupMap = get(columnLookupMap) - // 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, - }) - } - - // Sort rows by order + // Extract value of each selected cell 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) - ) + for (let row of $selectedCells) { + const rowValues = [] + for (let cellId of row) { + const { id, field } = parseCellID(cellId) + const rowIndex = $rowLookupMap[id] + const row = { + ...$rows[rowIndex], + ...$rowChangeCache[id], + } + rowValues.push(row[field]) + } + value.push(rowValues) } // Update state @@ -141,42 +124,26 @@ export const createActions = context => { if (!get(pasteAllowed)) { return } - const $clipboard = get(clipboard) + const { value, multiCellCopy } = get(clipboard) 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 cellIds = Object.keys($selectedCells) - const multiCellPaste = cellIds.length > 1 + const $selectedCellCount = get(selectedCellCount) + const multiCellPaste = $selectedCellCount > 1 - if ($clipboard.multiCellCopy) { + // Choose paste strategy + if (multiCellCopy) { if (multiCellPaste) { // 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 - // 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 + // Build change map 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] || [])) { + for (let rowIdx = 0; rowIdx < rowExtent; rowIdx++) { + for (let colIdx = 0; colIdx < colExtent; colIdx++) { + const cellId = $selectedCells[rowIdx][colIdx] + const { id, field } = parseCellID(cellId) if (!changeMap[id]) { changeMap[id] = {} } @@ -192,17 +159,19 @@ export const createActions = context => { 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] = {} + for (let row of $selectedCells) { + for (let cellId of row) { + const { id, field } = parseCellID(cellId) + if (!changeMap[id]) { + changeMap[id] = {} + } + changeMap[id][field] = value } - changeMap[id][field] = $clipboard.value } await rows.actions.bulkUpdate(changeMap) } else { // Single to single - $focusedCellAPI.setValue($clipboard.value) + $focusedCellAPI.setValue(value) } } } diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index 8e4414e363..eb966d2559 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -40,8 +40,8 @@ const DependencyOrderedStores = [ Users, Menu, Pagination, - Clipboard, Config, + Clipboard, Notifications, Cache, ] diff --git a/packages/frontend-core/src/components/grid/stores/menu.js b/packages/frontend-core/src/components/grid/stores/menu.js index 295604b275..d9e45b19a1 100644 --- a/packages/frontend-core/src/components/grid/stores/menu.js +++ b/packages/frontend-core/src/components/grid/stores/menu.js @@ -21,7 +21,7 @@ export const createActions = context => { gridID, selectedRows, selectedRowCount, - selectedCells, + selectedCellMap, selectedCellCount, } = context @@ -52,7 +52,7 @@ export const createActions = context => { // Check if there are multiple cells selected, and if this is one of them let multiCellMode = false if (!multiRowMode && get(selectedCellCount) > 1) { - if (get(selectedCells)[cellId]) { + if (get(selectedCellMap)[cellId]) { multiCellMode = true } } diff --git a/packages/frontend-core/src/components/grid/stores/ui.js b/packages/frontend-core/src/components/grid/stores/ui.js index f3858b5ede..30876b9a50 100644 --- a/packages/frontend-core/src/components/grid/stores/ui.js +++ b/packages/frontend-core/src/components/grid/stores/ui.js @@ -109,7 +109,7 @@ export const deriveStores = context => { ([$cellSelection, $rowLookupMap, $columnLookupMap]) => { const { sourceCellId, targetCellId } = $cellSelection if (!sourceCellId || !targetCellId || sourceCellId === targetCellId) { - return {} + return [] } const $rows = get(rows) const $allVisibleColumns = get(allVisibleColumns) @@ -130,24 +130,36 @@ export const deriveStores = context => { const lowerColIndex = Math.min(sourceColIndex, targetColIndex) const upperColIndex = Math.max(sourceColIndex, targetColIndex) - // Build map of all cells inside these bounds - let map = {} - let rowId, colName, cellId + // Build 2 dimensional array of all cells inside these bounds + let cells = [] + let rowId, colName for (let rowIdx = lowerRowIndex; rowIdx <= upperRowIndex; rowIdx++) { + let rowCells = [] for (let colIdx = lowerColIndex; colIdx <= upperColIndex; colIdx++) { rowId = $rows[rowIdx]._id colName = $allVisibleColumns[colIdx].name - cellId = getCellID(rowId, colName) - map[cellId] = { rowIdx, colIdx } + rowCells.push(getCellID(rowId, colName)) } + 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 - const selectedCellCount = derived(selectedCells, $selectedCells => { - return Object.keys($selectedCells).length + const selectedCellCount = derived(selectedCellMap, $selectedCellMap => { + return Object.keys($selectedCellMap).length }) return { @@ -158,6 +170,7 @@ export const deriveStores = context => { selectedRowCount, isSelectingCells, selectedCells, + selectedCellMap, selectedCellCount, } } @@ -272,9 +285,9 @@ export const createActions = context => { selectedCells: { ...selectedCells, actions: { - start: startCellSelection, - update: updateCellSelection, - stop: stopCellSelection, + startSelecting: startCellSelection, + updateTarget: updateCellSelection, + stopSelecting: stopCellSelection, clear: clearCellSelection, }, }, diff --git a/packages/frontend-core/src/components/grid/stores/users.js b/packages/frontend-core/src/components/grid/stores/users.js index 7dd7a69592..64c1e27835 100644 --- a/packages/frontend-core/src/components/grid/stores/users.js +++ b/packages/frontend-core/src/components/grid/stores/users.js @@ -25,7 +25,7 @@ export const deriveStores = context => { // Generate a lookup map of cell ID to the user that has it selected, to make // lookups inside cells extremely fast - const selectedCellMap = derived( + const userCellMap = derived( [users, focusedCellId], ([$users, $focusedCellId]) => { let map = {} @@ -40,7 +40,7 @@ export const deriveStores = context => { ) return { - selectedCellMap, + userCellMap, } }