1 parent
c892c8d1b0
commit
278429ab07
18 files changed
+337
-11
No files matched your search
@@ -6453,6 +6453,7 @@ function copyExtractorLabel(extractor: DataGridCopyExtractorId): string {
|
||||
"sql-in-list": t("grid.copyExtractorSqlInList"),
|
||||
"sql-inserts": t("grid.copyExtractorSqlInserts"),
|
||||
"sql-updates": t("grid.copyExtractorSqlUpdates"),
|
||||
"sql-select": t("grid.copyExtractorSqlSelect"),
|
||||
"where-clause": t("grid.copyExtractorWhereClause"),
|
||||
markdown: "Markdown",
|
||||
html: "HTML",
|
||||
|
||||
@@ -264,6 +264,106 @@ describe("useDataGridExport prepared row statements", () => {
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("UPDATE users SET active = TRUE WHERE id = 7;");
|
||||
});
|
||||
|
||||
it("builds a SELECT request for exactly one explicitly selected cell", async () => {
|
||||
const matrix: CellSelectionMatrix = {
|
||||
rowIndexes: [0],
|
||||
columnIndexes: [1],
|
||||
columns: ["name"],
|
||||
rows: [["Ada"]],
|
||||
};
|
||||
const state = createExportState(editableTable, ["id", "name"], matrix, [7, "Ada"]);
|
||||
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("includes hidden identity support columns in a selected-row SELECT request", async () => {
|
||||
const item = row([7, "Ada"]);
|
||||
const options: UseDataGridExportOptions = {
|
||||
columns: computed(() => ["name"]),
|
||||
displayItems: computed(() => [{ ...item, data: ["Ada"] }]),
|
||||
allColumns: computed(() => ["id", "name"]),
|
||||
allDisplayItems: computed(() => [item]),
|
||||
allSourceColumns: computed(() => ["id", "name"]),
|
||||
visibleColumnIndexes: computed(() => [1]),
|
||||
sql: computed(() => undefined),
|
||||
tableMeta: computed(() => editableTable),
|
||||
databaseType: computed(() => "mysql"),
|
||||
connectionId: computed(() => "connection-1"),
|
||||
database: computed(() => "dbx"),
|
||||
context: computed(() => "table-data"),
|
||||
sourceColumns: computed(() => ["name"]),
|
||||
columnTypes: computed(() => ["varchar"]),
|
||||
whereInput: computed(() => undefined),
|
||||
orderBy: computed(() => undefined),
|
||||
exportBatchSize: computed(() => 1000),
|
||||
hasCellSelection: computed(() => false),
|
||||
selectedCells: computed(() => ({ columns: [], rows: [] })),
|
||||
selectedCellMatrix: computed(() => null),
|
||||
selectedRange: computed(() => null),
|
||||
contextCell: ref(null),
|
||||
contextSelectionIsSynthetic: ref(false),
|
||||
getRowItem: (rowId) => (rowId === item.id ? item : undefined),
|
||||
selectedRowIds: ref(new Set([item.id])),
|
||||
hasRowSelection: computed(() => true),
|
||||
};
|
||||
const state = useDataGridExport(options);
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "SELECT * FROM users WHERE id = 7;", 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: "id" })],
|
||||
selectedColumnIndexes: [0],
|
||||
rows: [[7]],
|
||||
selectionKind: "rows",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to every source column when a selected-row identity is NULL", async () => {
|
||||
const state = createExportState(editableTable, ["id", "name"], undefined, [null, "Ada"], undefined, undefined, [1]);
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "SELECT * FROM users WHERE id IS NULL AND name = 'Ada';", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, columnCount: 2 });
|
||||
|
||||
expect(state.canCopyWithExtractor("sql-select")).toBe(true);
|
||||
await state.copyWithExtractor("sql-select");
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
extractor: "sql-select",
|
||||
columns: [expect.objectContaining({ sourceName: "id" }), expect.objectContaining({ sourceName: "name" })],
|
||||
selectedColumnIndexes: [0, 1],
|
||||
rows: [[null, "Ada"]],
|
||||
selectionKind: "rows",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("disables SELECT copy for multiple cells or a missing table target", () => {
|
||||
const matrix: CellSelectionMatrix = {
|
||||
rowIndexes: [0],
|
||||
columnIndexes: [0, 1],
|
||||
columns: ["id", "name"],
|
||||
rows: [[7, "Ada"]],
|
||||
};
|
||||
expect(createExportState(editableTable, ["id", "name"], matrix, [7, "Ada"]).canCopyWithExtractor("sql-select")).toBe(false);
|
||||
expect(createExportState({ ...editableTable, tableName: "" }, ["id", "name"], { ...matrix, columnIndexes: [1], columns: ["name"], rows: [["Ada"]] }, [7, "Ada"]).canCopyWithExtractor("sql-select")).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves drag-reordered visible column order when copying", async () => {
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValue({ text: "x", mimeType: "text/csv", fileExtension: "csv", rowCount: 1, columnCount: 3 });
|
||||
const matrix: CellSelectionMatrix = {
|
||||
|
||||
@@ -165,6 +165,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
copyInsertTargetLabel,
|
||||
sourceColumns,
|
||||
databaseType,
|
||||
identifierQuote,
|
||||
connectionId,
|
||||
database,
|
||||
context,
|
||||
@@ -592,6 +593,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
columnTypes,
|
||||
extractorOptions: extractorOptionsOption,
|
||||
databaseType,
|
||||
identifierQuote,
|
||||
tableMeta,
|
||||
hasCellSelection,
|
||||
selectedCells,
|
||||
|
||||
@@ -38,6 +38,7 @@ interface UseDataGridExtractorOptions {
|
||||
columnTypes: ComputedRef<Array<string | undefined> | undefined>;
|
||||
extractorOptions?: ComputedRef<DataGridExtractorOptions>;
|
||||
databaseType: ComputedRef<DatabaseType | undefined>;
|
||||
identifierQuote?: ComputedRef<string | undefined>;
|
||||
tableMeta: ComputedRef<DataGridTableMeta | undefined>;
|
||||
hasCellSelection: ComputedRef<boolean>;
|
||||
selectedCells: ComputedRef<SelectionData>;
|
||||
@@ -108,13 +109,18 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
selectedSourceIndexes = dedupeColumnIndexes(matrix.columnIndexes.map((index) => visibleIndexes[index] ?? index)).filter((index) => index < fullColumns.length);
|
||||
if (options.hasColumnSelection.value) selectionKind = "columns";
|
||||
} else if (hasRightClickContext && options.contextCell.value) {
|
||||
// Right-click on a cell with no (or synthetic single-cell) selection: copy the full row.
|
||||
// A SELECT targets the clicked cell; existing extractors keep their full-row context behavior.
|
||||
const item = fullItemsById.get(options.contextCell.value.rowId);
|
||||
if (item && !item.isDraft) {
|
||||
sourceRows = [item.data];
|
||||
sourceRowIds = [item.id];
|
||||
selectedSourceIndexes = dedupeColumnIndexes(visibleIndexes).filter((index) => index < fullColumns.length);
|
||||
selectionKind = "rows";
|
||||
if (extractor === "sql-select") {
|
||||
const sourceIndex = visibleIndexes[options.contextCell.value.col];
|
||||
selectedSourceIndexes = sourceIndex === undefined ? [] : [sourceIndex];
|
||||
} else {
|
||||
selectedSourceIndexes = dedupeColumnIndexes(visibleIndexes).filter((index) => index < fullColumns.length);
|
||||
selectionKind = "rows";
|
||||
}
|
||||
}
|
||||
} else if (matrix) {
|
||||
// Single-cell matrix without a context cell — still honor the explicit selection.
|
||||
@@ -127,7 +133,13 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
|
||||
if (sourceRows.length === 0 || selectedSourceIndexes.length === 0) return null;
|
||||
const requiredSourceIndexes = [...selectedSourceIndexes];
|
||||
if (extractor === "sql-updates") {
|
||||
if (extractor === "sql-select" && selectionKind === "rows") {
|
||||
const primaryKeys = options.tableMeta.value?.primaryKeys ?? [];
|
||||
const identityIndexes = primaryKeys.map((primaryKey) => fullColumns.findIndex((displayName, index) => normalizeName(sourceNames?.[index] ?? displayName) === normalizeName(primaryKey)));
|
||||
const hasCompleteIdentity = primaryKeys.length > 0 && identityIndexes.every((index) => index >= 0) && sourceRows.every((row) => identityIndexes.every((index) => row[index] !== null && row[index] !== undefined));
|
||||
selectedSourceIndexes = hasCompleteIdentity ? identityIndexes : [...fullColumns.keys()];
|
||||
requiredSourceIndexes.splice(0, requiredSourceIndexes.length, ...selectedSourceIndexes);
|
||||
} else if (extractor === "sql-updates") {
|
||||
for (const primaryKey of options.tableMeta.value?.primaryKeys ?? []) {
|
||||
const primaryKeyIndex = fullColumns.findIndex((displayName, index) => normalizeName(sourceNames?.[index] ?? displayName) === normalizeName(primaryKey));
|
||||
if (primaryKeyIndex >= 0 && !requiredSourceIndexes.includes(primaryKeyIndex)) requiredSourceIndexes.push(primaryKeyIndex);
|
||||
@@ -154,6 +166,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
version: DATA_GRID_EXTRACTOR_CONTRACT_VERSION,
|
||||
extractor,
|
||||
databaseType: descriptor.category === "sql" ? options.databaseType.value : undefined,
|
||||
identifierQuote: descriptor.category === "sql" ? options.identifierQuote?.value : undefined,
|
||||
tableMeta,
|
||||
columns,
|
||||
selectedColumnIndexes,
|
||||
@@ -222,6 +235,21 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
|
||||
}
|
||||
return canBuildSqlUpdateRequest();
|
||||
}
|
||||
if (extractor === "sql-select") {
|
||||
const matrix = options.selectedCellMatrix.value;
|
||||
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;
|
||||
} else if (!options.contextCell.value || !options.contextSelectionIsSynthetic.value) {
|
||||
return false;
|
||||
}
|
||||
const request = buildRequest(extractor, extractorOptions);
|
||||
if (!request?.tableMeta?.tableName.trim() || request.rows.length !== 1) return false;
|
||||
if (request.selectionKind === "columns") return false;
|
||||
if (request.selectionKind === "cells" && request.selectedColumnIndexes.length !== 1) return false;
|
||||
return request.columns.length > 0 && request.columns.every((column) => !!column.sourceName?.trim());
|
||||
}
|
||||
if (extractor === "raw") {
|
||||
const request = buildRequest(extractor, extractorOptions);
|
||||
return request !== null && request.rows.length * request.selectedColumnIndexes.length === 1;
|
||||
|
||||
@@ -1485,6 +1485,7 @@ export default {
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "SQL Inserts (with Primary Keys)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "SQL Inserts (without Primary Keys)",
|
||||
copyExtractorSqlUpdates: "SQL Updates",
|
||||
copyExtractorSqlSelect: "SQL Select",
|
||||
copyExtractorWhereClause: "Where Clause",
|
||||
copyExtractorPipeSeparated: "Pipe-separated",
|
||||
copyExtractorPretty: "Pretty Table",
|
||||
|
||||
@@ -1818,6 +1818,7 @@ export default withEnglishFallback({
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "Sentencias SQL INSERT (con claves primarias)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "Sentencias SQL INSERT (sin claves primarias)",
|
||||
copyExtractorSqlUpdates: "Sentencias SQL UPDATE",
|
||||
copyExtractorSqlSelect: "Sentencia SQL SELECT",
|
||||
copyExtractorWhereClause: "Cláusula WHERE",
|
||||
copyExtractorPipeSeparated: "Separado por barra vertical",
|
||||
copyExtractorPretty: "Tabla formateada",
|
||||
|
||||
@@ -1816,6 +1816,7 @@ export default withEnglishFallback({
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "Istruzioni SQL INSERT (con chiavi primarie)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "Istruzioni SQL INSERT (senza chiavi primarie)",
|
||||
copyExtractorSqlUpdates: "Istruzioni SQL UPDATE",
|
||||
copyExtractorSqlSelect: "Istruzione SQL SELECT",
|
||||
copyExtractorWhereClause: "Clausola WHERE",
|
||||
copyExtractorPipeSeparated: "Separato da pipe",
|
||||
copyExtractorPretty: "Testo tabellare",
|
||||
|
||||
@@ -1838,6 +1838,7 @@ export default withEnglishFallback({
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "SQL INSERT 文(主キーを含む)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "SQL INSERT 文(主キーを除外)",
|
||||
copyExtractorSqlUpdates: "SQL UPDATE 文",
|
||||
copyExtractorSqlSelect: "SQL SELECT 文",
|
||||
copyExtractorWhereClause: "WHERE 条件",
|
||||
copyExtractorPipeSeparated: "パイプ区切り",
|
||||
copyExtractorPretty: "テーブルテキスト",
|
||||
|
||||
@@ -1371,6 +1371,7 @@ export default withEnglishFallback({
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "SQL INSERT (기본 키 포함)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "SQL INSERT (기본 키 제외)",
|
||||
copyExtractorSqlUpdates: "SQL UPDATE",
|
||||
copyExtractorSqlSelect: "SQL SELECT",
|
||||
copyExtractorWhereClause: "WHERE 절",
|
||||
copyExtractorPipeSeparated: "파이프(|) 구분",
|
||||
copyExtractorPretty: "보기 좋은 표",
|
||||
|
||||
@@ -1818,6 +1818,7 @@ export default withEnglishFallback({
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "Instruções SQL INSERT (com chaves primárias)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "Instruções SQL INSERT (sem chaves primárias)",
|
||||
copyExtractorSqlUpdates: "Instruções SQL UPDATE",
|
||||
copyExtractorSqlSelect: "Instrução SQL SELECT",
|
||||
copyExtractorWhereClause: "Condição WHERE",
|
||||
copyExtractorPipeSeparated: "Separado por pipe",
|
||||
copyExtractorPretty: "Tabela formatada",
|
||||
|
||||
@@ -1409,6 +1409,7 @@ export default withEnglishFallback({
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "SQL INSERT 语句(含主键)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "SQL INSERT 语句(不含主键)",
|
||||
copyExtractorSqlUpdates: "SQL UPDATE 语句",
|
||||
copyExtractorSqlSelect: "SQL SELECT 语句",
|
||||
copyExtractorWhereClause: "WHERE 条件",
|
||||
copyExtractorPipeSeparated: "竖线分隔",
|
||||
copyExtractorPretty: "表格文本",
|
||||
|
||||
@@ -1822,6 +1822,7 @@ export default withEnglishFallback({
|
||||
copyExtractorSqlInsertsWithPrimaryKeys: "SQL INSERT 語句(含主鍵)",
|
||||
copyExtractorSqlInsertsWithoutPrimaryKeys: "SQL INSERT 語句(不含主鍵)",
|
||||
copyExtractorSqlUpdates: "SQL UPDATE 語句",
|
||||
copyExtractorSqlSelect: "SQL SELECT 語句",
|
||||
copyExtractorWhereClause: "WHERE 條件",
|
||||
copyExtractorPipeSeparated: "豎線分隔",
|
||||
copyExtractorPretty: "表格文本",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import type { DataGridCopyInsertMode, DataGridTableMeta } from "@/lib/dataGrid/dataGridSql";
|
||||
|
||||
export const DATA_GRID_COPY_EXTRACTOR_IDS = ["raw", "tsv", "tsv-with-headers", "csv", "csv-with-headers", "pipe-separated", "dsv", "json", "json-lines", "one-row", "sql-in-list", "sql-inserts", "sql-updates", "where-clause", "markdown", "html", "xml", "pretty"] as const;
|
||||
export const DATA_GRID_COPY_EXTRACTOR_IDS = ["raw", "tsv", "tsv-with-headers", "csv", "csv-with-headers", "pipe-separated", "dsv", "json", "json-lines", "one-row", "sql-in-list", "sql-inserts", "sql-updates", "sql-select", "where-clause", "markdown", "html", "xml", "pretty"] as const;
|
||||
|
||||
export type DataGridCopyExtractorId = (typeof DATA_GRID_COPY_EXTRACTOR_IDS)[number];
|
||||
export type DataGridCopyPreference = "smart" | Exclude<DataGridCopyExtractorId, "raw">;
|
||||
@@ -44,6 +44,7 @@ export const DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS: Record<DataGridCopyExtractorI
|
||||
"sql-in-list": { category: "sql", separatorBefore: true },
|
||||
"sql-inserts": { category: "sql" },
|
||||
"sql-updates": { category: "sql" },
|
||||
"sql-select": { category: "sql" },
|
||||
"where-clause": { category: "sql" },
|
||||
markdown: { category: "document", separatorBefore: true },
|
||||
html: { category: "document" },
|
||||
@@ -65,7 +66,7 @@ const NO_WHERE_CLAUSE_DATABASE_TYPES: ReadonlyArray<string> = ["neo4j", "tdengin
|
||||
export function extractorUnavailableForDatabase(extractor: DataGridCopyExtractorId, databaseType: string | undefined | null): boolean {
|
||||
const db = databaseType ?? "";
|
||||
if (extractor === "sql-updates") return NO_SQL_UPDATE_DATABASE_TYPES.includes(db);
|
||||
if (extractor === "where-clause") return NO_WHERE_CLAUSE_DATABASE_TYPES.includes(db);
|
||||
if (extractor === "where-clause" || extractor === "sql-select") return NO_WHERE_CLAUSE_DATABASE_TYPES.includes(db);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -100,6 +101,7 @@ export interface DataGridExtractRequest {
|
||||
version: typeof DATA_GRID_EXTRACTOR_CONTRACT_VERSION;
|
||||
extractor: DataGridCopyExtractorId;
|
||||
databaseType?: DatabaseType;
|
||||
identifierQuote?: string;
|
||||
tableMeta?: DataGridTableMeta;
|
||||
columns: DataGridExtractColumn[];
|
||||
selectedColumnIndexes: number[];
|
||||
|
||||
Reference in new issue
Block a user