diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index e296226fb..62d915c6f 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -7437,7 +7437,6 @@ function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleC contextHeaderColumnIndex.value = null; contextCell.value = { rowId, rowIndex, col: colIdx }; if (hasRowSelection.value && isRowSelected(rowId)) { - clearCellSelection(); void prefetchCopyStatements(); return; } @@ -7576,9 +7575,6 @@ function onRowContext(rowId: number, rowIndex: number) { contextHeaderColumn.value = null; contextHeaderColumnIndex.value = null; contextCell.value = { rowId, rowIndex, col: -1 }; - if (hasRowSelection.value && isRowSelected(rowId)) { - clearCellSelection(); - } if (!isRowSelected(rowId)) { clearCellSelection(); selectedRowIds.value = new Set([rowId]); diff --git a/apps/desktop/src/composables/__tests__/useDataGridSelection.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridSelection.spec.ts new file mode 100644 index 000000000..3a5ebe4ea --- /dev/null +++ b/apps/desktop/src/composables/__tests__/useDataGridSelection.spec.ts @@ -0,0 +1,66 @@ +import { computed, ref } from "vue"; +import { describe, expect, it } from "vitest"; +import { useDataGridSelection } from "@/composables/useDataGridSelection"; + +function createSelection() { + const columns = computed(() => ["id", "name", "email"]); + const displayItems = computed(() => + [1, 2, 3, 4].map((id, index) => ({ + id, + sourceIndex: index, + data: [id, `name-${id}`, `user-${id}@example.com`], + isNew: false, + isDraft: false, + isDeleted: false, + isDirtyCol: [false, false, false], + status: "clean", + })), + ); + + return useDataGridSelection({ + columns, + displayItems, + editingCell: ref(null), + showTranspose: ref(false), + transposeRowIndex: ref(null), + gridRef: ref(undefined), + }); +} + +function rowEvent(options: { meta?: boolean; shift?: boolean } = {}): MouseEvent { + return { + metaKey: !!options.meta, + ctrlKey: !!options.meta, + shiftKey: !!options.shift, + } as MouseEvent; +} + +describe("useDataGridSelection", () => { + it("creates a whole-row cell range for contiguous meta row selections", () => { + const selection = createSelection(); + + selection.handleRowClick(1, 2, rowEvent({ meta: true })); + selection.handleRowClick(2, 3, rowEvent({ meta: true })); + selection.handleRowClick(3, 4, rowEvent({ meta: true })); + + expect(selection.selectedRowIds.value).toEqual(new Set([2, 3, 4])); + expect(selection.selectedRange.value).toEqual({ + startRow: 1, + endRow: 3, + startCol: 0, + endCol: 2, + }); + expect(selection.hasCellSelection.value).toBe(true); + }); + + it("does not create a rectangular cell range for non-contiguous meta row selections", () => { + const selection = createSelection(); + + selection.handleRowClick(0, 1, rowEvent({ meta: true })); + selection.handleRowClick(2, 3, rowEvent({ meta: true })); + + expect(selection.selectedRowIds.value).toEqual(new Set([1, 3])); + expect(selection.selectedRange.value).toBeNull(); + expect(selection.hasCellSelection.value).toBe(false); + }); +}); diff --git a/apps/desktop/src/composables/useDataGridSelection.ts b/apps/desktop/src/composables/useDataGridSelection.ts index 7fac596dd..2c6fa74a6 100644 --- a/apps/desktop/src/composables/useDataGridSelection.ts +++ b/apps/desktop/src/composables/useDataGridSelection.ts @@ -213,6 +213,7 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) { next.add(rowId); } selectedRowIds.value = next; + selectContiguousRowIds(next); lastClickedRowIndex.value = rowIndex; } else if (isShift && lastClickedRowIndex.value !== null) { const start = Math.min(lastClickedRowIndex.value, rowIndex); @@ -231,6 +232,30 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) { } } + function selectContiguousRowIds(rowIds: Set) { + if (rowIds.size === 0) { + clearCellSelection(); + return; + } + + const indexes = displayItems.value.reduce((selectedIndexes, item, index) => { + if (rowIds.has(item.id)) selectedIndexes.push(index); + return selectedIndexes; + }, []); + if (indexes.length !== rowIds.size) { + clearCellSelection(); + return; + } + + const start = Math.min(...indexes); + const end = Math.max(...indexes); + if (end - start + 1 !== indexes.length) { + clearCellSelection(); + return; + } + selectRows(start, end); + } + function finishCellSelection() { isSelectingCells.value = false; document.removeEventListener("mouseup", finishCellSelection);