fix(data-grid): preserve scoped context selections
This commit is contained in:
4 files changed
+32
-17
No files matched your search
@@ -10154,6 +10154,7 @@ function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleC
|
|||||||
}
|
}
|
||||||
clearRowSelection();
|
clearRowSelection();
|
||||||
if (!cellIsSelected(rowIndex, visibleColIdx)) {
|
if (!cellIsSelected(rowIndex, visibleColIdx)) {
|
||||||
|
clearCellSelection();
|
||||||
selectSingleCell(rowIndex, visibleColIdx);
|
selectSingleCell(rowIndex, visibleColIdx);
|
||||||
contextSelectionIsSynthetic.value = true;
|
contextSelectionIsSynthetic.value = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ function createMongoExportState(options: {
|
|||||||
selectedCellMatrix?: CellSelectionMatrix;
|
selectedCellMatrix?: CellSelectionMatrix;
|
||||||
selectedRowIds?: Set<number>;
|
selectedRowIds?: Set<number>;
|
||||||
mongoUpdateTarget?: false;
|
mongoUpdateTarget?: false;
|
||||||
|
contextColumn?: number;
|
||||||
|
syntheticContext?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const items = options.items ?? [options.item];
|
const items = options.items ?? [options.item];
|
||||||
const selectedRowIds = options.selectedRowIds ?? new Set<number>();
|
const selectedRowIds = options.selectedRowIds ?? new Set<number>();
|
||||||
@@ -90,8 +92,8 @@ function createMongoExportState(options: {
|
|||||||
selectedCells: computed(() => options.selectedCellMatrix ?? { columns: [], rows: [] }),
|
selectedCells: computed(() => options.selectedCellMatrix ?? { columns: [], rows: [] }),
|
||||||
selectedCellMatrix: computed(() => options.selectedCellMatrix ?? null),
|
selectedCellMatrix: computed(() => options.selectedCellMatrix ?? null),
|
||||||
selectedRange: computed(() => null),
|
selectedRange: computed(() => null),
|
||||||
contextCell: ref({ rowId: options.item.id, rowIndex: 0, col: -1 }),
|
contextCell: ref({ rowId: options.item.id, rowIndex: 0, col: options.contextColumn ?? -1 }),
|
||||||
contextSelectionIsSynthetic: ref(false),
|
contextSelectionIsSynthetic: ref(options.syntheticContext ?? false),
|
||||||
getRowItem: (rowId) => items.find((item) => item.id === rowId),
|
getRowItem: (rowId) => items.find((item) => item.id === rowId),
|
||||||
selectedRowIds: ref(selectedRowIds),
|
selectedRowIds: ref(selectedRowIds),
|
||||||
hasRowSelection: computed(() => selectedRowIds.size > 0),
|
hasRowSelection: computed(() => selectedRowIds.size > 0),
|
||||||
@@ -637,8 +639,8 @@ describe("useDataGridExport prepared row statements", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copies the full row when right-clicking a cell with a synthetic single-cell selection", async () => {
|
it("copies only the clicked cell as TSV when right-clicking with a synthetic single-cell selection", async () => {
|
||||||
vi.mocked(extractDataGridSelection).mockResolvedValue({ text: "x", mimeType: "text/csv", fileExtension: "csv", rowCount: 1, columnCount: 3 });
|
vi.mocked(extractDataGridSelection).mockResolvedValue({ text: "x", mimeType: "text/tab-separated-values", fileExtension: "tsv", rowCount: 1, columnCount: 1 });
|
||||||
// A synthetic 1×1 selection (what right-click creates) — only column 0 of row 0.
|
// A synthetic 1×1 selection (what right-click creates) — only column 0 of row 0.
|
||||||
const matrix: CellSelectionMatrix = {
|
const matrix: CellSelectionMatrix = {
|
||||||
rowIndexes: [0],
|
rowIndexes: [0],
|
||||||
@@ -647,12 +649,14 @@ describe("useDataGridExport prepared row statements", () => {
|
|||||||
rows: [[1]],
|
rows: [[1]],
|
||||||
};
|
};
|
||||||
const state = createExportState(editableTable, ["id", "name", "note"], matrix, undefined, undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, [0, 1, 2], true);
|
const state = createExportState(editableTable, ["id", "name", "note"], matrix, undefined, undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, [0, 1, 2], true);
|
||||||
await state.copyWithExtractor("csv");
|
await state.copyWithExtractor("tsv");
|
||||||
|
|
||||||
// Despite the 1×1 matrix, the context-cell fallback should produce a full-row request (3 columns).
|
|
||||||
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
columns: [expect.objectContaining({ displayName: "id" }), expect.objectContaining({ displayName: "name" }), expect.objectContaining({ displayName: "note" })],
|
columns: [expect.objectContaining({ displayName: "id" })],
|
||||||
|
selectedColumnIndexes: [0],
|
||||||
|
rows: [[1]],
|
||||||
|
selectionKind: "cells",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1329,6 +1333,8 @@ describe("useDataGridExport prepared row statements", () => {
|
|||||||
columns: ["name"],
|
columns: ["name"],
|
||||||
rows: [[item.data[1]]],
|
rows: [[item.data[1]]],
|
||||||
},
|
},
|
||||||
|
contextColumn: 1,
|
||||||
|
syntheticContext: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(state.canCopyWithExtractor("sql-updates")).toBe(true);
|
expect(state.canCopyWithExtractor("sql-updates")).toBe(true);
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
|||||||
// happens to be the primary key. sql-select intentionally keeps using only
|
// happens to be the primary key. sql-select intentionally keeps using only
|
||||||
// the selected cell via the matrix branch below.
|
// the selected cell via the matrix branch below.
|
||||||
const hasNonSyntheticContextCell = !!options.contextCell.value && options.contextCell.value.col >= 0 && !options.contextSelectionIsSynthetic.value && !!matrix && !isMultiCellSelection;
|
const hasNonSyntheticContextCell = !!options.contextCell.value && options.contextCell.value.col >= 0 && !options.contextSelectionIsSynthetic.value && !!matrix && !isMultiCellSelection;
|
||||||
|
// INSERT and UPDATE need the complete record to retain key columns and
|
||||||
|
// writable values. Other extractors should respect the actual cell range
|
||||||
|
// that a right-click creates, just like keyboard copy does.
|
||||||
|
const requiresFullRowContext = extractor === "sql-inserts" || (extractor === "sql-updates" && options.databaseType.value !== "mongodb");
|
||||||
|
|
||||||
if (contextPredicateCell) {
|
if (contextPredicateCell) {
|
||||||
const item = fullItemsById.get(contextPredicateCell.rowId);
|
const item = fullItemsById.get(contextPredicateCell.rowId);
|
||||||
@@ -137,20 +141,15 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
|||||||
sourceRowIds = items.map((item) => item.id);
|
sourceRowIds = items.map((item) => item.id);
|
||||||
selectedSourceIndexes = dedupeColumnIndexes(matrix.columnIndexes.map((index) => visibleIndexes[index] ?? index)).filter((index) => index < fullColumns.length);
|
selectedSourceIndexes = dedupeColumnIndexes(matrix.columnIndexes.map((index) => visibleIndexes[index] ?? index)).filter((index) => index < fullColumns.length);
|
||||||
if (options.hasColumnSelection.value) selectionKind = "columns";
|
if (options.hasColumnSelection.value) selectionKind = "columns";
|
||||||
} else if ((hasRightClickContext || (hasNonSyntheticContextCell && extractor !== "sql-select")) && options.contextCell.value) {
|
} else if (requiresFullRowContext && (hasRightClickContext || hasNonSyntheticContextCell) && options.contextCell.value) {
|
||||||
// A SELECT targets the clicked cell; other extractors use the full row
|
// SQL INSERT/UPDATE needs the full row so that right-clicking a selected
|
||||||
// so that right-clicking a selected PK cell still enables INSERT/UPDATE.
|
// primary-key cell still includes writable columns.
|
||||||
const item = fullItemsById.get(options.contextCell.value.rowId);
|
const item = fullItemsById.get(options.contextCell.value.rowId);
|
||||||
if (item && !item.isDraft) {
|
if (item && !item.isDraft) {
|
||||||
sourceRows = [item.data];
|
sourceRows = [item.data];
|
||||||
sourceRowIds = [item.id];
|
sourceRowIds = [item.id];
|
||||||
if (extractor === "sql-select") {
|
selectedSourceIndexes = dedupeColumnIndexes(visibleIndexes).filter((index) => index < fullColumns.length);
|
||||||
const sourceIndex = visibleIndexes[options.contextCell.value.col];
|
selectionKind = "rows";
|
||||||
selectedSourceIndexes = sourceIndex === undefined ? [] : [sourceIndex];
|
|
||||||
} else {
|
|
||||||
selectedSourceIndexes = dedupeColumnIndexes(visibleIndexes).filter((index) => index < fullColumns.length);
|
|
||||||
selectionKind = "rows";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (matrix) {
|
} else if (matrix) {
|
||||||
// Single-cell matrix without a context cell — still honor the explicit selection.
|
// Single-cell matrix without a context cell — still honor the explicit selection.
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ test("select-all context menus invalidate a stale specialized target", () => {
|
|||||||
assert.match(header, /@contextmenu="invalidateContextMenuTarget"/);
|
assert.match(header, /@contextmenu="invalidateContextMenuTarget"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("right-clicking outside the old cell or column selection resets it before selecting the target", () => {
|
||||||
|
const handler = dataGridSource.match(/function onCellContext\([^]*?\n\}/)?.[0] ?? "";
|
||||||
|
const clear = handler.indexOf("clearCellSelection();");
|
||||||
|
const select = handler.indexOf("selectSingleCell(rowIndex, visibleColIdx);");
|
||||||
|
|
||||||
|
assert.ok(clear >= 0);
|
||||||
|
assert.ok(clear < select);
|
||||||
|
});
|
||||||
|
|
||||||
test("menu-close invalidation is deferred and guarded from a newer open", () => {
|
test("menu-close invalidation is deferred and guarded from a newer open", () => {
|
||||||
const handler = dataGridSource.match(/function onGridContextMenuClose\(\) \{[^]*?\n\}/)?.[0] ?? "";
|
const handler = dataGridSource.match(/function onGridContextMenuClose\(\) \{[^]*?\n\}/)?.[0] ?? "";
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user