fix(data-grid): scope SQL extractors to active context
This commit is contained in:
4 files changed
+143
-9
No files matched your search
@@ -112,6 +112,7 @@ function createExportState(
|
||||
visibleColumnIndexes?: number[],
|
||||
isSyntheticContext = false,
|
||||
contextRowId?: number | null,
|
||||
contextColumn?: number,
|
||||
) {
|
||||
const rows = (rowDataList ?? [rowData ?? columns.map((column, index) => (column === "id" ? 1 : `value-${index}`))]).map((data, index) => ({ ...row(data), id: index + 1 }));
|
||||
const resolvedContextRowId = contextRowId === undefined ? (rows[0]?.id ?? null) : contextRowId;
|
||||
@@ -137,7 +138,7 @@ function createExportState(
|
||||
selectedCells: computed(() => selectedCellMatrix ?? selectedCellsOverride ?? { columns: [], rows: [] }),
|
||||
selectedCellMatrix: computed(() => selectedCellMatrix ?? null),
|
||||
selectedRange: computed(() => null),
|
||||
contextCell: ref(resolvedContextRowId === null ? null : { rowId: resolvedContextRowId, rowIndex: 0, col: isSyntheticContext ? 0 : -1 }),
|
||||
contextCell: ref(resolvedContextRowId === null ? null : { rowId: resolvedContextRowId, rowIndex: 0, col: contextColumn ?? (isSyntheticContext ? 0 : -1) }),
|
||||
contextSelectionIsSynthetic: ref(isSyntheticContext),
|
||||
getRowItem: (rowId) => rows.find((candidate) => candidate.id === rowId),
|
||||
selectedRowIds,
|
||||
@@ -288,6 +289,42 @@ describe("useDataGridExport prepared row statements", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the right-clicked cell for SQL SELECT even when its row is selected", async () => {
|
||||
const state = createExportState(editableTable, ["id", "name"], undefined, [7, "Ada"], undefined, undefined, [1], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, 1, 1);
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "SELECT * FROM users WHERE name = 'Ada';", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, columnCount: 1 });
|
||||
|
||||
expect(state.canCopyWithExtractor("sql-select")).toBe(true);
|
||||
await state.copyWithExtractor("sql-select");
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
extractor: "sql-select",
|
||||
columns: [expect.objectContaining({ sourceName: "name" })],
|
||||
selectedColumnIndexes: [0],
|
||||
rows: [["Ada"]],
|
||||
selectionKind: "cells",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses only the right-clicked cell for a WHERE clause", async () => {
|
||||
const state = createExportState(editableTable, ["id", "name"], undefined, [7, "Ada"], undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, true, 1, 1);
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "name = 'Ada'", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, columnCount: 1 });
|
||||
|
||||
expect(state.canCopyWithExtractor("where-clause")).toBe(true);
|
||||
await state.copyWithExtractor("where-clause");
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
extractor: "where-clause",
|
||||
columns: [expect.objectContaining({ sourceName: "name" })],
|
||||
selectedColumnIndexes: [0],
|
||||
rows: [["Ada"]],
|
||||
selectionKind: "cells",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes hidden identity support columns in a selected-row SELECT request", async () => {
|
||||
const item = row([7, "Ada"]);
|
||||
const options: UseDataGridExportOptions = {
|
||||
@@ -403,6 +440,17 @@ describe("useDataGridExport prepared row statements", () => {
|
||||
});
|
||||
|
||||
expect(state.canCopyWithExtractor("sql-select")).toBe(true);
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "SELECT * FROM users WHERE id = 7;", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, columnCount: 1 });
|
||||
|
||||
return state.copyWithExtractor("sql-select").then(() => {
|
||||
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
columns: [expect.objectContaining({ displayName: "id", sourceName: "id" })],
|
||||
selectedColumnIndexes: [0],
|
||||
rows: [[7]],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("enables INSERT/UPDATE when right-clicking a previously selected primary-key cell", () => {
|
||||
@@ -854,6 +902,40 @@ describe("useDataGridExport prepared row statements", () => {
|
||||
expect(toast).toHaveBeenCalledWith("grid.copyExtractorUnsupportedSelection", 5000);
|
||||
});
|
||||
|
||||
it("uses the right-clicked cell for SQL predicates despite an irregular discrete selection", async () => {
|
||||
const state = createExportState(
|
||||
editableTable,
|
||||
["id", "name"],
|
||||
undefined,
|
||||
undefined,
|
||||
{ columns: ["id", "name"], rows: [[1], ["Grace"]] },
|
||||
[
|
||||
[1, "Ada"],
|
||||
[2, "Grace"],
|
||||
],
|
||||
[],
|
||||
DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS,
|
||||
false,
|
||||
undefined,
|
||||
false,
|
||||
2,
|
||||
1,
|
||||
);
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "name = 'Grace'", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, columnCount: 1 });
|
||||
|
||||
expect(state.canCopyWithExtractor("where-clause")).toBe(true);
|
||||
await expect(state.copyWithExtractor("where-clause")).resolves.toBe(true);
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
extractor: "where-clause",
|
||||
columns: [expect.objectContaining({ sourceName: "name" })],
|
||||
selectedColumnIndexes: [0],
|
||||
rows: [["Grace"]],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("limits live extractor previews to the first 100 selected rows", async () => {
|
||||
const rows = Array.from({ length: 101 }, (_, index) => [index + 1, `name-${index + 1}`]);
|
||||
const matrix: CellSelectionMatrix = {
|
||||
|
||||
@@ -79,6 +79,10 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
return options.hasCellSelection.value ? options.selectedCells.value : null;
|
||||
}
|
||||
|
||||
function hasContextPredicateTarget(extractor: DataGridCopyExtractorId): boolean {
|
||||
return (extractor === "sql-select" || extractor === "where-clause") && (options.contextCell.value?.col ?? -1) >= 0;
|
||||
}
|
||||
|
||||
function buildRequest(extractor: DataGridCopyExtractorId, extractorOptions: DataGridExtractorOptions = options.extractorOptions?.value ?? DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS): DataGridExtractRequest | null {
|
||||
const visibleIndexes = options.visibleColumnIndexes.value;
|
||||
const sourceNames = options.allSourceColumns.value;
|
||||
@@ -91,9 +95,14 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
|
||||
const matrix = options.selectedCellMatrix.value;
|
||||
const isMultiCellSelection = !!matrix && (matrix.rowIndexes.length > 1 || matrix.columnIndexes.length > 1);
|
||||
const contextCell = options.contextCell.value;
|
||||
// A right-click sets contextCell.col to the clicked column (≥ 0); the test
|
||||
// harness and non-right-click paths leave it at -1.
|
||||
const hasRightClickContext = !!options.contextCell.value && options.contextSelectionIsSynthetic.value;
|
||||
const hasRightClickContext = !!contextCell && options.contextSelectionIsSynthetic.value;
|
||||
// SQL predicates generated from the context menu must describe the cell the
|
||||
// user right-clicked, even when an existing row or range selection remains
|
||||
// active underneath that menu.
|
||||
const contextPredicateCell = hasContextPredicateTarget(extractor) ? contextCell : null;
|
||||
// When the user already has a single-cell selection and right-clicks the same
|
||||
// cell, contextSelectionIsSynthetic is false but we still have a valid context
|
||||
// cell. For INSERT/UPDATE extractors, we include all visible non-PK columns
|
||||
@@ -102,7 +111,15 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
// the selected cell via the matrix branch below.
|
||||
const hasNonSyntheticContextCell = !!options.contextCell.value && options.contextCell.value.col >= 0 && !options.contextSelectionIsSynthetic.value && !!matrix && !isMultiCellSelection;
|
||||
|
||||
if (options.hasRowSelection.value && options.selectedRowIds.value.size > 0) {
|
||||
if (contextPredicateCell) {
|
||||
const item = fullItemsById.get(contextPredicateCell.rowId);
|
||||
const sourceIndex = visibleIndexes[contextPredicateCell.col];
|
||||
if (item && !item.isDraft && sourceIndex !== undefined) {
|
||||
sourceRows = [item.data];
|
||||
sourceRowIds = [item.id];
|
||||
selectedSourceIndexes = [sourceIndex];
|
||||
}
|
||||
} else if (options.hasRowSelection.value && options.selectedRowIds.value.size > 0) {
|
||||
const items = options.displayItems.value.filter((item) => options.selectedRowIds.value.has(item.id) && !item.isDraft);
|
||||
sourceRows = items.map((item) => (fullItemsById.get(item.id) ?? item).data);
|
||||
sourceRowIds = items.map((item) => item.id);
|
||||
@@ -156,7 +173,10 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
const columnTypesBySource = new Map(visibleIndexes.map((sourceIndex, visibleIndex) => [sourceIndex, options.columnTypes.value?.[visibleIndex]]));
|
||||
const columns = requiredSourceIndexes.map((sourceIndex, compactIndex) => ({
|
||||
displayName: fullColumns[sourceIndex],
|
||||
sourceName: sourceNames?.[sourceIndex],
|
||||
// Query results without source metadata still expose their database column
|
||||
// names as display names. Keep the request mapping aligned with the
|
||||
// availability check so SQL SELECT can use that safe fallback.
|
||||
sourceName: sourceNames?.[sourceIndex] ?? fullColumns[sourceIndex],
|
||||
sourceIndex: compactIndex,
|
||||
}));
|
||||
const descriptor = DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor];
|
||||
@@ -227,7 +247,8 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
}
|
||||
|
||||
function canCopyWithExtractor(extractor: DataGridCopyExtractorId, extractorOptions: DataGridExtractorOptions = options.extractorOptions?.value ?? DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS): boolean {
|
||||
if (hasUnsupportedDiscreteSelection.value) return false;
|
||||
const contextPredicateTarget = hasContextPredicateTarget(extractor);
|
||||
if (hasUnsupportedDiscreteSelection.value && !contextPredicateTarget) return false;
|
||||
if (!options.hasRowSelection.value && !options.hasCellSelection.value && !options.contextCell.value) return false;
|
||||
if (extractorUnavailableForDatabase(extractor, options.databaseType.value)) return false;
|
||||
if (extractor === "sql-inserts") {
|
||||
@@ -244,7 +265,9 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
}
|
||||
if (extractor === "sql-select") {
|
||||
const matrix = options.selectedCellMatrix.value;
|
||||
if (options.hasRowSelection.value) {
|
||||
if (contextPredicateTarget) {
|
||||
// The context-menu target is validated by buildRequest below.
|
||||
} else if (options.hasRowSelection.value) {
|
||||
if (options.selectedRowIds.value.size !== 1) return false;
|
||||
} else if (matrix) {
|
||||
if (matrix.rowIndexes.length !== 1 || matrix.columnIndexes.length !== 1) return false;
|
||||
@@ -257,6 +280,9 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
if (request.selectionKind === "cells" && request.selectedColumnIndexes.length !== 1) return false;
|
||||
return request.columns.length > 0 && request.columns.every((column) => !!(column.sourceName ?? column.displayName)?.trim());
|
||||
}
|
||||
if (extractor === "where-clause" && contextPredicateTarget) {
|
||||
return buildRequest(extractor, extractorOptions) !== null;
|
||||
}
|
||||
if (extractor === "raw") {
|
||||
const request = buildRequest(extractor, extractorOptions);
|
||||
return request !== null && request.rows.length * request.selectedColumnIndexes.length === 1;
|
||||
@@ -278,7 +304,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
}
|
||||
|
||||
async function copyWithExtractor(extractor: DataGridCopyExtractorId, extractorOptions: DataGridExtractorOptions = options.extractorOptions?.value ?? DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS): Promise<boolean> {
|
||||
if (hasUnsupportedDiscreteSelection.value) {
|
||||
if (hasUnsupportedDiscreteSelection.value && !hasContextPredicateTarget(extractor)) {
|
||||
toast(t("grid.copyExtractorUnsupportedSelection"), 5000);
|
||||
return false;
|
||||
}
|
||||
@@ -317,7 +343,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
}
|
||||
|
||||
async function previewWithExtractor(extractor: DataGridCopyExtractorId, extractorOptions: DataGridExtractorOptions): Promise<DataGridExtractPreview> {
|
||||
if (hasUnsupportedDiscreteSelection.value) throw new Error(t("grid.copyExtractorUnsupportedSelection"));
|
||||
if (hasUnsupportedDiscreteSelection.value && !hasContextPredicateTarget(extractor)) throw new Error(t("grid.copyExtractorUnsupportedSelection"));
|
||||
const initialRequest = buildRequest(extractor, extractorOptions);
|
||||
if (!initialRequest) throw new Error(t("grid.copyExtractorEmptySelection"));
|
||||
const sourceRowCount = initialRequest.rows.length;
|
||||
|
||||
Reference in new issue
Block a user