feat(grid): add copy as UPDATE action
This commit is contained in:
parent
35bfad8aec
commit
7592def541
|
|
@ -1077,6 +1077,10 @@ const visibleColumnIndexes = computed(() =>
|
|||
visibleColumnIndexesForFilter(displayableColumnIndexes.value, hiddenColumnIndexes.value),
|
||||
);
|
||||
const visibleColumns = computed(() => visibleColumnIndexes.value.map((index) => props.result.columns[index]));
|
||||
const visibleSourceColumns = computed(() => {
|
||||
if (!props.sourceColumns || props.sourceColumns.length !== props.result.columns.length) return undefined;
|
||||
return visibleColumnIndexes.value.map((index) => props.sourceColumns?.[index]);
|
||||
});
|
||||
const visibleColumnCount = computed(() => visibleColumnIndexes.value.length);
|
||||
const displayableColumnCount = computed(() => displayableColumnIndexes.value.length);
|
||||
const hiddenColumnCount = computed(() => displayableColumnCount.value - visibleColumnCount.value);
|
||||
|
|
@ -2080,6 +2084,8 @@ const {
|
|||
copyCell,
|
||||
copyRow,
|
||||
copyRowAsInsert,
|
||||
copyRowAsUpdate,
|
||||
canCopyRowAsUpdate,
|
||||
copyAll,
|
||||
copySelectionTsv,
|
||||
copySelectionCsv,
|
||||
|
|
@ -2094,10 +2100,9 @@ const {
|
|||
columns: visibleColumns,
|
||||
displayItems: visibleDisplayItems,
|
||||
sql: computed(() => props.sql),
|
||||
tableMeta: computed(() =>
|
||||
props.tableMeta ? { schema: props.tableMeta.schema, tableName: props.tableMeta.tableName } : undefined,
|
||||
),
|
||||
tableMeta: computed(() => (props.tableMeta ? { ...props.tableMeta } : undefined)),
|
||||
databaseType: computed(() => props.databaseType),
|
||||
sourceColumns: visibleSourceColumns,
|
||||
hasCellSelection,
|
||||
selectedCells,
|
||||
selectedRange,
|
||||
|
|
@ -4313,6 +4318,9 @@ defineExpose({
|
|||
<ContextMenuItem @click="copyRowAsInsert">
|
||||
{{ isMultiRow ? t("grid.copyRowsInsert", { count: multiRowCount }) : t("grid.copyRowInsert") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="canCopyRowAsUpdate" @click="copyRowAsUpdate">
|
||||
{{ isMultiRow ? t("grid.copyRowsUpdate", { count: multiRowCount }) : t("grid.copyRowUpdate") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyAll">{{ t("grid.copyAll") }}</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ComputedRef, Ref } from "vue";
|
||||
import { computed, type ComputedRef, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { formatCsv, formatJson } from "@/lib/exportFormats";
|
||||
|
|
@ -14,6 +14,8 @@ import { useToast } from "@/composables/useToast";
|
|||
import { displayCellValue, type CellValue } from "@/lib/cellValue";
|
||||
import { tryStartExclusiveActivation, type ActionActivationGuard } from "@/lib/actionActivation";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
import { buildDataGridCopyUpdateStatements } from "@/lib/dataGridSql";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
interface RowItem {
|
||||
id: number;
|
||||
|
|
@ -30,8 +32,9 @@ export interface UseDataGridExportOptions {
|
|||
columns: ComputedRef<string[]>;
|
||||
displayItems: ComputedRef<RowItem[]>;
|
||||
sql: ComputedRef<string | undefined>;
|
||||
tableMeta: ComputedRef<{ schema?: string; tableName: string } | undefined>;
|
||||
databaseType: ComputedRef<string | undefined>;
|
||||
tableMeta: ComputedRef<{ schema?: string; tableName: string; primaryKeys: string[] } | undefined>;
|
||||
databaseType: ComputedRef<DatabaseType | undefined>;
|
||||
sourceColumns: ComputedRef<Array<string | undefined> | undefined>;
|
||||
hasCellSelection: ComputedRef<boolean>;
|
||||
selectedCells: ComputedRef<SelectionData>;
|
||||
selectedRange: ComputedRef<CellSelectionRange | null>;
|
||||
|
|
@ -55,6 +58,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
displayItems,
|
||||
sql,
|
||||
tableMeta,
|
||||
sourceColumns,
|
||||
databaseType,
|
||||
hasCellSelection,
|
||||
selectedCells,
|
||||
selectedRange,
|
||||
|
|
@ -81,6 +86,23 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
return displayItems.value.filter((item) => rowIdSet.has(item.id));
|
||||
}
|
||||
|
||||
function targetedRows(): RowItem[] {
|
||||
if (hasRowSelection.value && selectedRowIds.value.size > 0) {
|
||||
return displayItems.value.filter((item) => selectedRowIds.value.has(item.id));
|
||||
}
|
||||
const range = selectedRange.value;
|
||||
if (range && range.startRow !== range.endRow) {
|
||||
return displayItems.value.slice(range.startRow, range.endRow + 1);
|
||||
}
|
||||
if (!contextCell.value) return [];
|
||||
const item = getRowItem(contextCell.value.rowId);
|
||||
return item ? [item] : [];
|
||||
}
|
||||
|
||||
function updateEligibleRows(): RowItem[] {
|
||||
return targetedRows().filter((item) => !item.isNew && !item.isDeleted);
|
||||
}
|
||||
|
||||
// --- Selection copy functions ---
|
||||
async function copySelectionTsv() {
|
||||
if (!hasCellSelection.value) return;
|
||||
|
|
@ -174,6 +196,34 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await copyText(`INSERT INTO ${table} (${cols}) VALUES (${vals});`);
|
||||
}
|
||||
|
||||
async function copyRowAsUpdate() {
|
||||
if (!tableMeta.value?.primaryKeys.length) return;
|
||||
const statements = buildDataGridCopyUpdateStatements({
|
||||
databaseType: databaseType.value,
|
||||
tableMeta: tableMeta.value,
|
||||
columns: columns.value,
|
||||
sourceColumns: sourceColumns.value,
|
||||
rows: updateEligibleRows().map((item) => item.data),
|
||||
});
|
||||
if (!statements.length) return;
|
||||
await copyText(statements.join("\n"));
|
||||
}
|
||||
|
||||
const canCopyRowAsUpdate = computed(() => {
|
||||
if (!tableMeta.value?.primaryKeys.length) return false;
|
||||
const rows = updateEligibleRows();
|
||||
if (!rows.length) return false;
|
||||
return (
|
||||
buildDataGridCopyUpdateStatements({
|
||||
databaseType: databaseType.value,
|
||||
tableMeta: tableMeta.value,
|
||||
columns: columns.value,
|
||||
sourceColumns: sourceColumns.value,
|
||||
rows: [rows[0].data],
|
||||
}).length > 0
|
||||
);
|
||||
});
|
||||
|
||||
async function copyAll() {
|
||||
const header = columns.value.join("\t");
|
||||
const body = displayItems.value.map((item) => item.data.map((c) => displayCellValue(c)).join("\t")).join("\n");
|
||||
|
|
@ -320,6 +370,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
copyCell,
|
||||
copyRow,
|
||||
copyRowAsInsert,
|
||||
copyRowAsUpdate,
|
||||
canCopyRowAsUpdate,
|
||||
copyAll,
|
||||
copySelectionTsv,
|
||||
copySelectionCsv,
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ export default {
|
|||
copyCell: "Copy Cell",
|
||||
copyRow: "Copy Row (JSON)",
|
||||
copyRowInsert: "Copy as INSERT",
|
||||
copyRowUpdate: "Copy as UPDATE",
|
||||
copyAll: "Copy All (TSV)",
|
||||
selection: "Selection",
|
||||
copySelectionTsv: "Copy Selection (TSV)",
|
||||
|
|
@ -344,6 +345,7 @@ export default {
|
|||
restoreRows: "Restore {count} Rows",
|
||||
copyRows: "Copy {count} Rows (JSON)",
|
||||
copyRowsInsert: "Copy {count} Rows as INSERT",
|
||||
copyRowsUpdate: "Copy {count} Rows as UPDATE",
|
||||
selectedRows: "{count} rows selected",
|
||||
restoreRow: "Restore Row",
|
||||
statusClean: "Clean",
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ export default {
|
|||
copyCell: "Copiar celda",
|
||||
copyRow: "Copiar fila (JSON)",
|
||||
copyRowInsert: "Copiar como INSERT",
|
||||
copyRowUpdate: "Copiar como UPDATE",
|
||||
copyAll: "Copiar todo (TSV)",
|
||||
selection: "Selección",
|
||||
copySelectionTsv: "Copiar selección (TSV)",
|
||||
|
|
@ -312,6 +313,7 @@ export default {
|
|||
restoreRows: "Restaurar {count} filas",
|
||||
copyRows: "Copiar {count} filas (JSON)",
|
||||
copyRowsInsert: "Copiar {count} filas como INSERT",
|
||||
copyRowsUpdate: "Copiar {count} filas como UPDATE",
|
||||
selectedRows: "{count} filas seleccionadas",
|
||||
restoreRow: "Restaurar fila",
|
||||
statusClean: "Sin cambios",
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ export default {
|
|||
copyCell: "复制单元格",
|
||||
copyRow: "复制行 (JSON)",
|
||||
copyRowInsert: "复制为 INSERT 语句",
|
||||
copyRowUpdate: "复制为 UPDATE 语句",
|
||||
copyAll: "复制全部 (TSV)",
|
||||
selection: "选区",
|
||||
copySelectionTsv: "复制选区 (TSV)",
|
||||
|
|
@ -341,6 +342,7 @@ export default {
|
|||
restoreRows: "恢复 {count} 行",
|
||||
copyRows: "复制 {count} 行 (JSON)",
|
||||
copyRowsInsert: "复制 {count} 行为 INSERT",
|
||||
copyRowsUpdate: "复制 {count} 行为 UPDATE",
|
||||
selectedRows: "已选 {count} 行",
|
||||
restoreRow: "恢复行",
|
||||
statusClean: "未改",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,14 @@ export interface DataGridSaveStatementOptions {
|
|||
newRows: GridCellValue[][];
|
||||
}
|
||||
|
||||
export interface DataGridCopyUpdateStatementOptions {
|
||||
databaseType?: DatabaseType;
|
||||
tableMeta: DataGridTableMeta;
|
||||
columns: string[];
|
||||
sourceColumns?: Array<string | undefined>;
|
||||
rows: GridCellValue[][];
|
||||
}
|
||||
|
||||
export interface DataGridSaveValidationOptions {
|
||||
databaseType?: DatabaseType;
|
||||
tableMeta?: DataGridTableMeta;
|
||||
|
|
@ -157,6 +165,49 @@ export function buildDataGridSaveStatements(options: DataGridSaveStatementOption
|
|||
return statements;
|
||||
}
|
||||
|
||||
export function buildDataGridCopyUpdateStatements(options: DataGridCopyUpdateStatementOptions): string[] {
|
||||
if (options.databaseType === "neo4j" || options.databaseType === "tdengine") return [];
|
||||
const primaryKeys = options.tableMeta.primaryKeys;
|
||||
if (primaryKeys.length === 0) return [];
|
||||
|
||||
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
||||
const primaryKeyIndexes = primaryKeys.map((primaryKey) => findColumnIndex(saveColumns, primaryKey));
|
||||
if (primaryKeyIndexes.some((index) => index === -1)) return [];
|
||||
|
||||
const primaryKeySet = new Set(primaryKeys.map((primaryKey) => normalizeColumnName(primaryKey)));
|
||||
const writableIndexes = saveColumns
|
||||
.map((column, index) => ({ column, index }))
|
||||
.filter((entry): entry is { column: string; index: number } => !!entry.column)
|
||||
.filter((entry) => !primaryKeySet.has(normalizeColumnName(entry.column)))
|
||||
.filter((entry) => !isOracleRowId(options.databaseType, entry.column));
|
||||
|
||||
if (writableIndexes.length === 0) return [];
|
||||
|
||||
const table = qualifiedTableName({
|
||||
databaseType: options.databaseType,
|
||||
schema: options.tableMeta.schema,
|
||||
tableName: options.tableMeta.tableName,
|
||||
});
|
||||
|
||||
const statements: string[] = [];
|
||||
for (const row of options.rows) {
|
||||
if (primaryKeyIndexes.some((index) => row[index] === null || row[index] === undefined)) continue;
|
||||
const sets = writableIndexes
|
||||
.map(
|
||||
({ column, index }) =>
|
||||
`${quoteIdent(options.databaseType, column)} = ${formatGridSqlLiteral(row[index], options.databaseType)}`,
|
||||
)
|
||||
.join(", ");
|
||||
if (!sets) continue;
|
||||
const where = primaryKeys
|
||||
.map((primaryKey, index) => buildColumnPredicate(options.databaseType, primaryKey, row[primaryKeyIndexes[index]]))
|
||||
.join(" AND ");
|
||||
statements.push(`UPDATE ${table} SET ${sets} WHERE ${where};`);
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
function buildTdengineDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
|
||||
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
||||
const statements: string[] = [];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import {
|
||||
buildDataGridCopyUpdateStatements,
|
||||
buildDataGridRollbackStatements,
|
||||
buildDataGridSaveStatements,
|
||||
dataGridSaveExecutionSchema,
|
||||
|
|
@ -40,6 +41,35 @@ test("builds SQL Server grid save statements with schema and bracket quoting", (
|
|||
]);
|
||||
});
|
||||
|
||||
test("builds copy-as-update statements using primary keys and non-primary-key columns", () => {
|
||||
const statements = buildDataGridCopyUpdateStatements({
|
||||
databaseType: "postgres",
|
||||
tableMeta: {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
primaryKeys: ["id"],
|
||||
},
|
||||
columns: ["id", "name", "status"],
|
||||
rows: [[1, "Ada", "active"]],
|
||||
});
|
||||
|
||||
assert.deepEqual(statements, [`UPDATE "public"."users" SET "name" = 'Ada', "status" = 'active' WHERE "id" = 1;`]);
|
||||
});
|
||||
|
||||
test("skips copy-as-update statements when primary keys are unavailable", () => {
|
||||
const statements = buildDataGridCopyUpdateStatements({
|
||||
databaseType: "postgres",
|
||||
tableMeta: {
|
||||
tableName: "users",
|
||||
primaryKeys: [],
|
||||
},
|
||||
columns: ["id", "name"],
|
||||
rows: [[1, "Ada"]],
|
||||
});
|
||||
|
||||
assert.deepEqual(statements, []);
|
||||
});
|
||||
|
||||
test("builds Access grid save statements with backtick identifiers", () => {
|
||||
const statements = buildDataGridSaveStatements({
|
||||
databaseType: "access",
|
||||
|
|
|
|||
Loading…
Reference in New Issue