feat(sidebar): batch actions for multi selected nodes

This commit is contained in:
Mo-Moore authored and GitHub committed 2026-09-01 00:22:02 +08:00
1 parent 2f71a1a8aa
commit 69a032ed63
37 files changed
+1302 -159

No files matched your search

@@ -1,4 +1,5 @@
import type { DatabaseType } from "@/types/database";
import { COLUMN_NAME_COPY_SEPARATOR_VALUES, loadColumnNameCopySeparator, type ColumnNameCopySeparator, isColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
import { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier.ts";
@@ -37,18 +38,27 @@ export const DBX_TABLE_REFERENCE_DROP_EVENT = "dbx-table-reference-drop";
export const DBX_TABLE_REFERENCE_HOVER_EVENT = "dbx-table-reference-hover";
export const DBX_TABLE_REFERENCE_DRAG_END_EVENT = "dbx-table-reference-drag-end";
export interface QueryEditorTableReferenceEntry {
schema?: string;
tableName: string;
}
export interface QueryEditorTableReferencePayload {
kind: "dbx-table-reference";
connectionId: string;
database: string;
schema?: string;
tableName?: string;
tableReferences?: QueryEditorTableReferenceEntry[];
columnName?: string;
/** 多个结果列一起拖入时按选择顺序排列;单列沿用 columnName。 */
columnNames?: string[];
referenceType?: "database" | "table" | "column";
databaseType?: DatabaseType;
driverProfile?: string;
columnNameSeparator?: ColumnNameCopySeparator;
tableNameSeparator?: ColumnNameCopySeparator;
includeTableSchema?: boolean;
}
export interface QueryEditorTableReferenceDropDetail {
@@ -62,16 +72,38 @@ export interface QueryEditorTableReferenceHoverDetail {
clientY: number;
}
export interface TableReferenceInsertOptions {
tableNameSeparator?: ColumnNameCopySeparator;
includeTableSchema?: boolean;
columnNameSeparator?: ColumnNameCopySeparator;
}
function normalizeColumnNames(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map((name) => (typeof name === "string" ? name.trim() : "")).filter((name) => name.length > 0);
}
function normalizeTableReferences(value: unknown): QueryEditorTableReferenceEntry[] {
if (!Array.isArray(value)) return [];
const entries: QueryEditorTableReferenceEntry[] = [];
for (const item of value) {
if (!item || typeof item !== "object") continue;
const tableName = typeof (item as QueryEditorTableReferenceEntry).tableName === "string" ? (item as QueryEditorTableReferenceEntry).tableName.trim() : "";
if (!tableName) continue;
const schema = typeof (item as QueryEditorTableReferenceEntry).schema === "string" ? (item as QueryEditorTableReferenceEntry).schema : undefined;
entries.push(schema ? { schema, tableName } : { tableName });
}
return entries;
}
/** schema/databaseType/driverProfile 为可选元数据,payload 各构造路径共用同一拷贝规则。 */
function applyOptionalReferenceMeta(payload: QueryEditorTableReferencePayload, source: Partial<QueryEditorTableReferencePayload>) {
if (typeof source.schema === "string" && source.schema) payload.schema = source.schema;
if (source.databaseType) payload.databaseType = source.databaseType;
if (typeof source.driverProfile === "string" && source.driverProfile) payload.driverProfile = source.driverProfile;
if (isColumnNameCopySeparator(source.columnNameSeparator)) payload.columnNameSeparator = source.columnNameSeparator;
if (isColumnNameCopySeparator(source.tableNameSeparator)) payload.tableNameSeparator = source.tableNameSeparator;
if (source.includeTableSchema === true) payload.includeTableSchema = true;
}
let activeTableReferencePayload: QueryEditorTableReferencePayload | null = null;
@@ -107,7 +139,40 @@ export function createTableReferencePayload(options: {
return payload;
}
export function createColumnReferencePayload(options: { connectionId?: string; database?: string; schema?: string; columnNames?: readonly (string | undefined | null)[]; databaseType?: DatabaseType }): QueryEditorTableReferencePayload | null {
export function createMultiTableReferencePayload(options: {
connectionId?: string;
database?: string;
tableReferences?: readonly QueryEditorTableReferenceEntry[];
databaseType?: DatabaseType;
driverProfile?: string;
tableNameSeparator?: ColumnNameCopySeparator;
includeTableSchema?: boolean;
}): QueryEditorTableReferencePayload | null {
if (!options.connectionId || options.database == null) return null;
const tableReferences = normalizeTableReferences(options.tableReferences);
if (tableReferences.length === 0) return null;
const payload: QueryEditorTableReferencePayload = {
kind: "dbx-table-reference",
connectionId: options.connectionId,
database: options.database,
referenceType: "table",
};
if (tableReferences.length === 1) {
payload.tableName = tableReferences[0]!.tableName;
if (tableReferences[0]!.schema) payload.schema = tableReferences[0]!.schema;
} else {
payload.tableReferences = tableReferences;
}
applyOptionalReferenceMeta(payload, {
databaseType: options.databaseType,
driverProfile: options.driverProfile,
tableNameSeparator: options.tableNameSeparator,
includeTableSchema: options.includeTableSchema,
});
return payload;
}
export function createColumnReferencePayload(options: { connectionId?: string; database?: string; schema?: string; columnNames?: readonly (string | undefined | null)[]; databaseType?: DatabaseType; columnNameSeparator?: ColumnNameCopySeparator }): QueryEditorTableReferencePayload | null {
const columnNames = normalizeColumnNames(options.columnNames);
if (!options.connectionId || options.database == null || columnNames.length === 0) return null;
const payload: QueryEditorTableReferencePayload = {
@@ -143,10 +208,8 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu
if (typeof parsed.driverProfile === "string" && parsed.driverProfile) payload.driverProfile = parsed.driverProfile;
return payload;
}
if (typeof parsed.tableName !== "string" || !parsed.tableName) {
// 无来源表的纯列引用(如从查询结果列头拖入),tableName 非必需。
const columnNames = normalizeColumnNames(parsed.columnNames);
if (columnNames.length === 0) return null;
const columnNames = normalizeColumnNames(parsed.columnNames);
if (columnNames.length > 0 && (parsed.referenceType === "column" || !parsed.tableName)) {
const payload: QueryEditorTableReferencePayload = {
kind: "dbx-table-reference",
connectionId: parsed.connectionId,
@@ -157,18 +220,34 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu
applyOptionalReferenceMeta(payload, parsed);
return payload;
}
const tableReferences = normalizeTableReferences(parsed.tableReferences);
if (tableReferences.length > 1) {
const payload: QueryEditorTableReferencePayload = {
kind: "dbx-table-reference",
connectionId: parsed.connectionId,
database: parsed.database,
referenceType: "table",
tableReferences,
};
applyOptionalReferenceMeta(payload, parsed);
return payload;
}
if (typeof parsed.tableName !== "string" || !parsed.tableName) {
return null;
}
const columnName = typeof parsed.columnName === "string" && parsed.columnName ? parsed.columnName : undefined;
const referenceType = parsed.referenceType === "column" || columnName ? "column" : "table";
if (referenceType === "column" && !columnName) return null;
const referenceType = parsed.referenceType === "column" || columnName || columnNames.length > 0 ? "column" : "table";
const payload: QueryEditorTableReferencePayload = {
kind: "dbx-table-reference",
connectionId: parsed.connectionId,
database: parsed.database,
tableName: parsed.tableName,
};
if (referenceType === "column" && columnName) {
payload.columnName = columnName;
if (referenceType === "column") {
payload.referenceType = "column";
if (columnNames.length > 0) payload.columnNames = columnNames;
else if (columnName) payload.columnName = columnName;
else return null;
}
applyOptionalReferenceMeta(payload, parsed);
return payload;
@@ -211,14 +290,40 @@ export function createTableReferenceDragEndEvent(): Event {
return new Event(DBX_TABLE_REFERENCE_DRAG_END_EVENT);
}
export function tableReferenceInsertText(payload: QueryEditorTableReferencePayload, fallbackDatabaseType?: DatabaseType): string {
function separatorValue(payload: QueryEditorTableReferencePayload, options: TableReferenceInsertOptions | undefined, kind: "table" | "column"): string {
const fromPayload = kind === "table" ? payload.tableNameSeparator : payload.columnNameSeparator;
const fromOptions = kind === "table" ? options?.tableNameSeparator : options?.columnNameSeparator;
const fallback = kind === "column" ? loadColumnNameCopySeparator() : "comma";
const separator = fromOptions ?? fromPayload ?? fallback;
return COLUMN_NAME_COPY_SEPARATOR_VALUES[separator] ?? (kind === "column" ? "\t" : ",");
}
export function tableReferenceInsertText(payload: QueryEditorTableReferencePayload, fallbackDatabaseType?: DatabaseType, options?: TableReferenceInsertOptions): string {
const databaseType = payload.databaseType ?? fallbackDatabaseType;
if (payload.referenceType === "database") {
return quoteTableIdentifier(databaseType, payload.database);
}
const columnNames = payload.columnNames?.length ? payload.columnNames : payload.columnName ? [payload.columnName] : [];
if (payload.referenceType === "column" && columnNames.length > 0) {
return columnNames.map((name) => quoteColumnReferenceName(databaseType, name)).join(",\n");
return columnNames.map((name) => quoteColumnReferenceName(databaseType, name)).join(separatorValue(payload, options, "column"));
}
const includeSchema = payload.includeTableSchema ?? options?.includeTableSchema ?? false;
const tableSeparator = separatorValue(payload, options, "table");
const tableReferences = payload.tableReferences?.length ? payload.tableReferences : payload.tableName ? [{ schema: payload.schema, tableName: payload.tableName }] : [];
if (tableReferences.length > 1 || (tableReferences.length === 1 && includeSchema)) {
return tableReferences
.map((entry) =>
includeSchema
? qualifiedTableName({
databaseType,
driverProfile: payload.driverProfile,
schema: entry.schema,
tableName: entry.tableName,
database: payload.database,
})
: entry.tableName,
)
.join(tableSeparator);
}
const tableName = payload.tableName || payload.database;
return qualifiedTableName({