feat(sidebar): batch actions for multi selected nodes
This commit is contained in:
1 parent
2f71a1a8aa
commit
69a032ed63
37 files changed
+1302
-159
No files matched your search
@@ -944,8 +944,10 @@ function sendSelectionToAi(sql: string) {
|
||||
|
||||
let addToAiRequestId = 0;
|
||||
|
||||
async function addToAi(node: TreeNode) {
|
||||
if ((node.type !== "connection" && node.type !== "database" && node.type !== "table") || !node.connectionId) return;
|
||||
async function addToAi(nodesInput: TreeNode | TreeNode[]) {
|
||||
const nodes = Array.isArray(nodesInput) ? nodesInput : [nodesInput];
|
||||
const node = nodes[0];
|
||||
if (!node || (node.type !== "connection" && node.type !== "database" && node.type !== "table") || !node.connectionId) return;
|
||||
const connection = connectionStore.getConfig(node.connectionId);
|
||||
if (!connection) return;
|
||||
const requestId = ++addToAiRequestId;
|
||||
@@ -984,10 +986,12 @@ async function addToAi(node: TreeNode) {
|
||||
queryStore.createTab(node.connectionId, target.database, undefined, "query", target.schema, undefined, target.catalog);
|
||||
}
|
||||
|
||||
const tableMentions = nodes.filter((entry) => entry.type === "table" && !!entry.label).map((entry) => ({ schema: entry.schema, table: entry.label }));
|
||||
|
||||
openRightSidebarPanel("ai");
|
||||
invokeWhenAiReady((handle) => {
|
||||
if (contextChanged) handle.clearContextReferences();
|
||||
if (node.type === "table") handle.addTableMention({ schema: node.schema, table: node.label });
|
||||
for (const mention of tableMentions) handle.addTableMention(mention);
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
|
||||
@@ -272,6 +272,7 @@ const props = defineProps<{
|
||||
prefillDatabase?: string;
|
||||
prefillSchema?: string;
|
||||
focusTableName?: string;
|
||||
focusTableNames?: string[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -395,6 +396,8 @@ const tableMap = computed(() => new Map(tables.value.map((table) => [table.name,
|
||||
const allRelationships = computed(() => buildDiagramRelationships(tables.value, customRelationships.value));
|
||||
|
||||
const relatedTableNames = computed(() => {
|
||||
const selected = props.focusTableNames?.filter((name) => name.trim());
|
||||
if (selected && selected.length > 1) return new Set(selected);
|
||||
const focus = props.focusTableName;
|
||||
const names = new Set<string>();
|
||||
if (!focus) return names;
|
||||
@@ -411,6 +414,9 @@ const visibleTables = computed(() => {
|
||||
tables.value.filter((table) => !table.pendingDrop),
|
||||
tableSearch.value,
|
||||
);
|
||||
if ((props.focusTableNames?.length ?? 0) > 1 && !showAllTables.value && !tableSearch.value.trim()) {
|
||||
return filtered.filter((table) => relatedTableNames.value.has(table.name));
|
||||
}
|
||||
if (props.focusTableName && !showAllTables.value && !tableSearch.value.trim()) {
|
||||
return filtered.filter((table) => relatedTableNames.value.has(table.name));
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ import {
|
||||
import { eventToModifierOnlyShortcut, eventToShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
|
||||
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
|
||||
import { COLUMN_NAME_COPY_SEPARATOR_LABELS, COLUMN_NAME_COPY_SEPARATOR_OPTIONS, isColumnNameCopySeparator, type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
|
||||
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame";
|
||||
@@ -524,6 +525,8 @@ const editShowTableDdlHoverPreview = ref(settingsStore.editorSettings.showTableD
|
||||
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
|
||||
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
|
||||
const editSidebarCopyTableNameSeparator = ref<ColumnNameCopySeparator>(settingsStore.editorSettings.sidebarCopyTableNameSeparator);
|
||||
const editSidebarCopyTableNameIncludeSchema = ref(settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema);
|
||||
const editRedisKeyTemplates = ref(normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n"));
|
||||
const editSidebarObjectInfoMode = ref<SidebarObjectInfoMode>(settingsStore.editorSettings.sidebarObjectInfoMode);
|
||||
const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll);
|
||||
@@ -672,6 +675,8 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
sidebarIndent: editSidebarIndent.value,
|
||||
sidebarFontSize: editSidebarFontSize.value,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
|
||||
sidebarCopyTableNameSeparator: editSidebarCopyTableNameSeparator.value,
|
||||
sidebarCopyTableNameIncludeSchema: editSidebarCopyTableNameIncludeSchema.value,
|
||||
redisKeyTemplates: normalizeRedisKeyTemplates(editRedisKeyTemplates.value),
|
||||
exportBatchSize: editExportBatchSize.value,
|
||||
globalDateTimeDisplayFormat: editGlobalDateTimeDisplayFormat.value,
|
||||
@@ -1066,6 +1071,8 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
|
||||
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
|
||||
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
|
||||
editSidebarCopyTableNameSeparator.value = settingsStore.editorSettings.sidebarCopyTableNameSeparator;
|
||||
editSidebarCopyTableNameIncludeSchema.value = settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema;
|
||||
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n");
|
||||
editSidebarObjectInfoMode.value = settingsStore.editorSettings.sidebarObjectInfoMode;
|
||||
editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll;
|
||||
@@ -1332,6 +1339,8 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editSidebarIndent.value = DEFAULT_EDITOR_SETTINGS.sidebarIndent;
|
||||
editSidebarFontSize.value = DEFAULT_EDITOR_SETTINGS.sidebarFontSize;
|
||||
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
|
||||
editSidebarCopyTableNameSeparator.value = DEFAULT_EDITOR_SETTINGS.sidebarCopyTableNameSeparator;
|
||||
editSidebarCopyTableNameIncludeSchema.value = DEFAULT_EDITOR_SETTINGS.sidebarCopyTableNameIncludeSchema;
|
||||
editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems };
|
||||
} else if (tab === "data") {
|
||||
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
|
||||
@@ -1466,6 +1475,8 @@ function resetAllDefaults() {
|
||||
editSidebarIndent.value = DEFAULT_EDITOR_SETTINGS.sidebarIndent;
|
||||
editSidebarFontSize.value = DEFAULT_EDITOR_SETTINGS.sidebarFontSize;
|
||||
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
|
||||
editSidebarCopyTableNameSeparator.value = DEFAULT_EDITOR_SETTINGS.sidebarCopyTableNameSeparator;
|
||||
editSidebarCopyTableNameIncludeSchema.value = DEFAULT_EDITOR_SETTINGS.sidebarCopyTableNameIncludeSchema;
|
||||
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
|
||||
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
|
||||
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
|
||||
@@ -5733,6 +5744,40 @@ onUnmounted(() => {
|
||||
{{ t("settings.sidebarHiddenTablePrefixesDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="sidebar-copy-table-name-separator">{{ t("settings.sidebarCopyTableNameSeparator") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.sidebarCopyTableNameSeparatorDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
:model-value="editSidebarCopyTableNameSeparator"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
if (isColumnNameCopySeparator(value)) editSidebarCopyTableNameSeparator = value;
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger id="sidebar-copy-table-name-separator" class="h-8 w-44 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" align="end">
|
||||
<SelectItem v-for="option in COLUMN_NAME_COPY_SEPARATOR_OPTIONS" :key="option" :value="option" class="font-mono text-xs">
|
||||
{{ COLUMN_NAME_COPY_SEPARATOR_LABELS[option] }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="sidebar-copy-table-name-include-schema">{{ t("settings.sidebarCopyTableNameIncludeSchema") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.sidebarCopyTableNameIncludeSchemaDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="sidebar-copy-table-name-include-schema" v-model="editSidebarCopyTableNameIncludeSchema" class="mt-0.5" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="sidebar-table-page-size">{{ t("settings.sidebarTablePageSize") }}</Label>
|
||||
|
||||
@@ -3360,7 +3360,11 @@ function hasDroppedTableReference(event: DragEvent) {
|
||||
|
||||
function insertTableReferencePayload(currentView: EditorViewType, payload: QueryEditorTableReferencePayload, coords?: { clientX: number; clientY: number }): boolean {
|
||||
if (props.readOnly) return false;
|
||||
const insertText = tableReferenceInsertText(payload, props.databaseType);
|
||||
const insertText = tableReferenceInsertText(payload, props.databaseType, {
|
||||
tableNameSeparator: settingsStore.editorSettings.sidebarCopyTableNameSeparator,
|
||||
columnNameSeparator: settingsStore.editorSettings.sidebarCopyTableNameSeparator,
|
||||
includeTableSchema: settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema,
|
||||
});
|
||||
const dropPos = coords ? currentView.posAtCoords({ x: coords.clientX, y: coords.clientY }) : null;
|
||||
const selection = currentView.state.selection.main;
|
||||
const from = dropPos ?? selection.from;
|
||||
|
||||
@@ -272,6 +272,7 @@ watch(
|
||||
:prefill-database="dialogs.diagramPrefillDatabase.value"
|
||||
:prefill-schema="dialogs.diagramPrefillSchema.value"
|
||||
:focus-table-name="dialogs.diagramFocusTableName.value"
|
||||
:focus-table-names="dialogs.diagramFocusTableNames.value"
|
||||
@open-target="emit('openDiagramTarget', $event)"
|
||||
/>
|
||||
<DatabaseDocsDialog v-if="dialogs.showDocsDialog.value" v-model:open="dialogs.showDocsDialog.value" :prefill-connection-id="dialogs.docsPrefillConnectionId.value" :prefill-database="dialogs.docsPrefillDatabase.value" :prefill-schema="dialogs.docsPrefillSchema.value" />
|
||||
|
||||
@@ -26,7 +26,7 @@ const emit = defineEmits<{
|
||||
startResize: [event: MouseEvent];
|
||||
collapse: [];
|
||||
"open-settings": [initialTab: string];
|
||||
"add-to-ai": [node: TreeNode];
|
||||
"add-to-ai": [nodes: TreeNode | TreeNode[]];
|
||||
}>();
|
||||
|
||||
type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip";
|
||||
@@ -293,7 +293,7 @@ defineExpose({ focusSearch, locateTabInSidebar });
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<ConnectionTree ref="connectionTreeRef" @open-settings="(initialTab) => emit('open-settings', initialTab)" @add-to-ai="(node) => emit('add-to-ai', node)" />
|
||||
<ConnectionTree ref="connectionTreeRef" @open-settings="(initialTab) => emit('open-settings', initialTab)" @add-to-ai="(nodes) => emit('add-to-ai', nodes)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-resize-handle panel-resize-handle--right" @mousedown="emit('startResize', $event)" />
|
||||
|
||||
@@ -29,10 +29,11 @@ import { createSidebarSearchLoadingTracker } from "@/lib/sidebar/sidebarSearchLo
|
||||
import { buildTableTreeNodes } from "@/lib/table/tableTree";
|
||||
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { sidebarNodeSupportsDdlView } from "@/lib/sidebar/sidebarTreeDdlShortcut";
|
||||
import { copyNameForTreeNode, objectSourceTargetForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { objectSourceTargetForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { formatSidebarTableCopyText } from "@/lib/sidebar/sidebarTableNameCopy";
|
||||
import { pruneTreeSelectionToVisibleNodeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
|
||||
import { isInternalDorisCatalog, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
@@ -116,7 +117,7 @@ const sidebarContextMenuRef = ref<{ close: () => void } | null>(null);
|
||||
const sidebarContextMenuItems = ref<ContextMenuItem[]>([]);
|
||||
const emit = defineEmits<{
|
||||
"open-settings": [initialTab: string];
|
||||
"add-to-ai": [node: TreeNode];
|
||||
"add-to-ai": [nodes: TreeNode | TreeNode[]];
|
||||
}>();
|
||||
|
||||
const sidebarContextMenuTarget = ref<SidebarActionTarget | null>(null);
|
||||
@@ -1959,7 +1960,7 @@ function openSidebarDdlForSelection(): boolean {
|
||||
const selectedNodeId = store.selectedTreeNodeId;
|
||||
const node = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null;
|
||||
if (!node || !sidebarNodeSupportsDdlView(node)) return false;
|
||||
openSidebarDdl(node);
|
||||
void sidebarTreeRuntimeHostRef.value?.openDdlForSelection?.(node, store.selectedTreeNodeIds);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2335,7 +2336,17 @@ function copySelectedSidebarNames(): boolean {
|
||||
})),
|
||||
}
|
||||
: null;
|
||||
copyToClipboard(nodes.map(copyNameForTreeNode).join("\n"))
|
||||
const activeNodeId = store.selectedTreeNodeId;
|
||||
const activeNode = activeNodeId ? (flatTreeIndex.value.nodeById.get(activeNodeId) ?? nodes[0]!) : nodes[0]!;
|
||||
const config = activeNode.connectionId ? store.getConfig(activeNode.connectionId) : undefined;
|
||||
const copyText = formatSidebarTableCopyText(activeNode, nodes, {
|
||||
separator: settingsStore.editorSettings.sidebarCopyTableNameSeparator,
|
||||
includeSchema: settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema,
|
||||
databaseType: activeNode.connectionId ? effectiveDatabaseTypeForConnection(config) : undefined,
|
||||
driverProfile: config?.driver_profile,
|
||||
identifierQuote: activeNode.connectionId ? store.connectionIdentifierQuote?.(activeNode.connectionId) : undefined,
|
||||
});
|
||||
copyToClipboard(copyText)
|
||||
.then(() => toast(t("connection.copied"), 2000))
|
||||
.catch((e: any) => toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000));
|
||||
return true;
|
||||
@@ -2420,7 +2431,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
|
||||
@open-visible-schemas="openSidebarVisibleSchemas"
|
||||
@open-visible-nacos-namespaces="openSidebarVisibleNacosNamespaces"
|
||||
@open-table-name-filters="openSidebarTableNameFilters"
|
||||
@add-to-ai="(node) => emit('add-to-ai', node)"
|
||||
@add-to-ai="(nodes) => emit('add-to-ai', nodes)"
|
||||
@request-connection-rename="startRenamingConnectionNode"
|
||||
@request-group-rename="startRenamingCreatedGroup"
|
||||
@request-saved-sql-rename="startRenamingSavedSqlNode"
|
||||
|
||||
@@ -88,6 +88,7 @@ import { buildXuguSchedulerJobSql, type XuguSchedulerJobAction } from "@/lib/dat
|
||||
import { elasticsearchClearIndexPreview, isElasticsearchClearConfirmed, isElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions";
|
||||
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
|
||||
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
|
||||
import { joinExportedDdls } from "@/lib/export/ddlExport";
|
||||
import { qualifiedTableName } from "@/lib/table/tableSelectSql";
|
||||
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
|
||||
import {
|
||||
@@ -177,8 +178,10 @@ import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty";
|
||||
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
|
||||
import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
|
||||
import { buildSidebarDdlTemplateSql, sidebarDdlTargetsForExecutionContext } from "@/lib/sidebar/sidebarDdlTemplate";
|
||||
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { buildSidebarDdlTemplateSql } from "@/lib/sidebar/sidebarDdlTemplate";
|
||||
import { resolveSidebarDdlTargets } from "@/lib/sidebar/sidebarDdlTargets";
|
||||
import { sidebarTableDataExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { formatSidebarTableCopyText, type FormatSidebarTableNamesOptions } from "@/lib/sidebar/sidebarTableNameCopy";
|
||||
import { supportsScheduledDatabaseBackup } from "@/lib/backup/scheduledDatabaseBackup";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
@@ -389,6 +392,7 @@ const { openAllDatabasesExport, openDataCompare, openDatabaseExport, openDatabas
|
||||
queryStore,
|
||||
settingsStore,
|
||||
tableChildObjectName: tableChildDropObjectName,
|
||||
acceptedSelectionIds: () => acceptedSelectionIds,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -409,7 +413,7 @@ const emit = defineEmits<{
|
||||
"open-visible-schemas": [node: TreeNode];
|
||||
"open-visible-nacos-namespaces": [node: TreeNode];
|
||||
"open-table-name-filters": [node: TreeNode];
|
||||
"add-to-ai": [node: TreeNode];
|
||||
"add-to-ai": [nodes: TreeNode | TreeNode[]];
|
||||
"open-danger-dialog": [request: SidebarDangerDialogRequest];
|
||||
"open-dialog-controller": [controller: Record<string, any> | null];
|
||||
"open-install-extension": [node: TreeNode];
|
||||
@@ -637,6 +641,21 @@ function databaseTypeForNode(node: TreeNode): DatabaseType | undefined {
|
||||
return node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
|
||||
}
|
||||
|
||||
function sidebarTableCopyFormatOptions(node: TreeNode = activeNode.value): FormatSidebarTableNamesOptions {
|
||||
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
||||
return {
|
||||
separator: settingsStore.editorSettings.sidebarCopyTableNameSeparator,
|
||||
includeSchema: settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema,
|
||||
databaseType: node.connectionId ? effectiveDatabaseTypeForConnection(config) : undefined,
|
||||
driverProfile: config?.driver_profile,
|
||||
identifierQuote: node.connectionId ? connectionStore.connectionIdentifierQuote?.(node.connectionId) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function formatSelectedTableNamesForClipboard(selectedNodes: readonly TreeNode[] = selectedTreeNodesInVisibleOrder()): string {
|
||||
return formatSidebarTableCopyText(activeNode.value, selectedNodes, sidebarTableCopyFormatOptions());
|
||||
}
|
||||
|
||||
function tableStructureDatabaseTypeForNode(node: TreeNode): DatabaseType | undefined {
|
||||
return node.connectionId ? tableStructureDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
|
||||
}
|
||||
@@ -1755,8 +1774,7 @@ function openRedisInstanceInfo() {
|
||||
queryStore.createTab(node.connectionId, "0", `${dbName} - ${t("contextMenu.instanceInfo")}`, "redis-dashboard");
|
||||
}
|
||||
|
||||
async function loadTemplateContext(allowView = false) {
|
||||
const node = activeNode.value;
|
||||
async function loadTemplateContext(allowView = false, node: TreeNode = activeNode.value) {
|
||||
if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return null;
|
||||
const isTableNode = node.type === "table";
|
||||
const isReadableObject = isTableNode || (allowView && (node.type === "view" || node.type === "materialized_view"));
|
||||
@@ -1791,6 +1809,20 @@ async function loadTemplateContext(allowView = false) {
|
||||
return { node, dbType, driverProfile: config?.driver_profile, identifierQuote, tableSchema, columns, tableType };
|
||||
}
|
||||
|
||||
async function openMultiTableSqlTemplate(targets: Array<TreeNode & { connectionId: string; database: string }>, buildSql: (context: NonNullable<Awaited<ReturnType<typeof loadTemplateContext>>>) => string, allowView: boolean, titlePrefix: string) {
|
||||
const parts: string[] = [];
|
||||
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
|
||||
for (const target of targets) {
|
||||
const context = await loadTemplateContext(allowView, target);
|
||||
if (!context) continue;
|
||||
parts.push(buildSql(context));
|
||||
}
|
||||
if (!parts.length) return;
|
||||
const sql = parts.length === 1 ? parts[0]! : joinExportedDdls(parts);
|
||||
const title = targets.length === 1 ? undefined : `${titlePrefix} - ${targets.map((target) => target.label).join(", ")}`;
|
||||
openSqlTemplateTab(tabTarget.connectionId, tabTarget.database, tabTarget.schema, tabTarget.catalog, sql, title);
|
||||
}
|
||||
|
||||
function openSqlTemplateTab(connectionId: string, database: string, schema: string | undefined, catalog: string | undefined, sql: string, title?: string) {
|
||||
const tabId = queryStore.createTab(connectionId, database, title, "query", schema, undefined, catalog, { forceWordWrap: true });
|
||||
queryStore.updateSql(tabId, sql);
|
||||
@@ -1798,6 +1830,27 @@ function openSqlTemplateTab(connectionId: string, database: string, schema: stri
|
||||
|
||||
async function newSelectTemplate() {
|
||||
try {
|
||||
const targets = selectedDdlTargets().filter((target) => target.type === "table" || target.type === "view" || target.type === "materialized_view");
|
||||
if (targets.length > 1) {
|
||||
await openMultiTableSqlTemplate(
|
||||
targets,
|
||||
(context) =>
|
||||
buildTableSelectTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
}),
|
||||
true,
|
||||
"SELECT",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const context = await loadTemplateContext(true);
|
||||
if (!context) return;
|
||||
const sql = buildTableSelectTemplate({
|
||||
@@ -1819,6 +1872,28 @@ async function newSelectTemplate() {
|
||||
|
||||
async function newInsertTemplate() {
|
||||
try {
|
||||
const targets = selectedDdlTargets().filter((target) => target.type === "table");
|
||||
if (targets.length > 1) {
|
||||
await openMultiTableSqlTemplate(
|
||||
targets,
|
||||
(context) =>
|
||||
buildTableInsertTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
tableType: context.tableType,
|
||||
}),
|
||||
false,
|
||||
"INSERT",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const context = await loadTemplateContext(false);
|
||||
if (!context) return;
|
||||
const sql = buildTableInsertTemplate({
|
||||
@@ -1841,6 +1916,27 @@ async function newInsertTemplate() {
|
||||
|
||||
async function newUpdateTemplate() {
|
||||
try {
|
||||
const targets = selectedDdlTargets().filter((target) => target.type === "table");
|
||||
if (targets.length > 1) {
|
||||
await openMultiTableSqlTemplate(
|
||||
targets,
|
||||
(context) =>
|
||||
buildTableUpdateTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
}),
|
||||
false,
|
||||
"UPDATE",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const context = await loadTemplateContext(false);
|
||||
if (!context) return;
|
||||
const sql = buildTableUpdateTemplate({
|
||||
@@ -1862,6 +1958,27 @@ async function newUpdateTemplate() {
|
||||
|
||||
async function newDeleteTemplate() {
|
||||
try {
|
||||
const targets = selectedDdlTargets().filter((target) => target.type === "table");
|
||||
if (targets.length > 1) {
|
||||
await openMultiTableSqlTemplate(
|
||||
targets,
|
||||
(context) =>
|
||||
buildTableDeleteTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
schema: context.tableSchema,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
tableName: context.node.label,
|
||||
columns: context.columns,
|
||||
}),
|
||||
false,
|
||||
"DELETE",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const context = await loadTemplateContext(false);
|
||||
if (!context) return;
|
||||
const sql = buildTableDeleteTemplate({
|
||||
@@ -1881,41 +1998,44 @@ async function newDeleteTemplate() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openSidebarMultiTableDdlTab(targets: Array<TreeNode & { connectionId: string; database: string }>) {
|
||||
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
|
||||
const sql = await buildSidebarDdlTemplateSql(
|
||||
targets,
|
||||
async (target) => {
|
||||
await connectionStore.ensureConnected(target.connectionId);
|
||||
const schema = target.schema || target.database;
|
||||
if (target.type === "table") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, undefined, target.catalog);
|
||||
if (target.type === "materialized_view") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, "MATERIALIZED_VIEW", target.catalog);
|
||||
const result = await api.getObjectSource(target.connectionId, target.database, schema, target.label, "VIEW");
|
||||
return buildViewDdl({
|
||||
databaseType: databaseTypeForNode(target),
|
||||
identifierQuote: connectionStore.connectionIdentifierQuote?.(target.connectionId),
|
||||
schema,
|
||||
name: target.label,
|
||||
source: result.source,
|
||||
});
|
||||
},
|
||||
(ddl, target) => formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseTypeForNode(target)), settingsStore.editorSettings.sqlFormatter),
|
||||
);
|
||||
connectionStore.activeConnectionId = tabTarget.connectionId;
|
||||
const title = `DDL - ${targets.map((target) => target.label).join(", ")}`;
|
||||
openSqlTemplateTab(tabTarget.connectionId, tabTarget.database, tabTarget.schema, tabTarget.catalog, sql, title);
|
||||
}
|
||||
|
||||
async function generateDdlTemplate() {
|
||||
if (currentDatabaseType() === "victoriametrics") return;
|
||||
const targets = selectedDdlTargets();
|
||||
if (!targets.length) return;
|
||||
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
|
||||
try {
|
||||
const sql = await buildSidebarDdlTemplateSql(
|
||||
targets,
|
||||
async (target) => {
|
||||
await connectionStore.ensureConnected(target.connectionId);
|
||||
const schema = target.schema || target.database;
|
||||
if (target.type === "table") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, undefined, target.catalog);
|
||||
if (target.type === "materialized_view") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, "MATERIALIZED_VIEW", target.catalog);
|
||||
const result = await api.getObjectSource(target.connectionId, target.database, schema, target.label, "VIEW");
|
||||
return buildViewDdl({
|
||||
databaseType: databaseTypeForNode(target),
|
||||
identifierQuote: connectionStore.connectionIdentifierQuote?.(target.connectionId),
|
||||
schema,
|
||||
name: target.label,
|
||||
source: result.source,
|
||||
});
|
||||
},
|
||||
(ddl, target) => formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseTypeForNode(target)), settingsStore.editorSettings.sqlFormatter),
|
||||
);
|
||||
connectionStore.activeConnectionId = tabTarget.connectionId;
|
||||
const title = `DDL - ${targets.map((target) => target.label).join(", ")}`;
|
||||
openSqlTemplateTab(tabTarget.connectionId, tabTarget.database, tabTarget.schema, tabTarget.catalog, sql, title);
|
||||
await openSidebarMultiTableDdlTab(targets);
|
||||
} catch (e: any) {
|
||||
toast(e?.message || String(e), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function selectedDdlTargets() {
|
||||
const targets = sidebarStructureExportTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds);
|
||||
return sidebarDdlTargetsForExecutionContext(activeNode.value as TreeNode & { connectionId: string; database: string }, targets);
|
||||
return resolveSidebarDdlTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds);
|
||||
}
|
||||
|
||||
async function openDdl() {
|
||||
@@ -1923,12 +2043,35 @@ async function openDdl() {
|
||||
const targets = selectedDdlTargets();
|
||||
if (!targets.length) return;
|
||||
if (targets.length > 1) {
|
||||
await exportStructure();
|
||||
try {
|
||||
await openSidebarMultiTableDdlTab(targets);
|
||||
} catch (e: any) {
|
||||
toast(e?.message || String(e), 5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emit("open-ddl", targets[0]!);
|
||||
}
|
||||
|
||||
async function openDdlForSelection(node: TreeNode, selectedNodeIds: readonly string[]): Promise<boolean> {
|
||||
if (node.type !== "table" && node.type !== "view" && node.type !== "materialized_view") return false;
|
||||
activateRuntimeNode(node);
|
||||
const dbType = node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
|
||||
if (dbType === "victoriametrics") return false;
|
||||
const targets = resolveSidebarDdlTargets(node, connectionStore.treeNodes, selectedNodeIds);
|
||||
if (!targets.length) return false;
|
||||
if (targets.length > 1) {
|
||||
try {
|
||||
await openSidebarMultiTableDdlTab(targets);
|
||||
} catch (e: any) {
|
||||
toast(e?.message || String(e), 5000);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
emit("open-ddl", targets[0]!);
|
||||
return true;
|
||||
}
|
||||
|
||||
function openElasticsearchIndexMetadata(kind: ElasticsearchIndexMetadataKind) {
|
||||
emit("open-elasticsearch-index-metadata", createSidebarActionTarget(activeNode.value), kind);
|
||||
}
|
||||
@@ -1944,9 +2087,8 @@ async function refresh() {
|
||||
}
|
||||
|
||||
async function copyName() {
|
||||
const node = activeNode.value;
|
||||
try {
|
||||
await copyToClipboard(copyNameForTreeNode(node));
|
||||
await copyToClipboard(formatSelectedTableNamesForClipboard());
|
||||
toast(t("connection.copied"), 2000);
|
||||
} catch (e: any) {
|
||||
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
||||
@@ -2038,7 +2180,7 @@ async function copySelectedNames() {
|
||||
}
|
||||
updateTreeClipboardForNodes(nodes);
|
||||
try {
|
||||
await copyToClipboard(nodes.map(copyNameForTreeNode).join("\n"));
|
||||
await copyToClipboard(formatSelectedTableNamesForClipboard(selectedNodes));
|
||||
toast(t("connection.copied"), 2000);
|
||||
} catch (e: any) {
|
||||
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
||||
@@ -4844,7 +4986,26 @@ const shortcutRefresh = "F5";
|
||||
|
||||
const shortcutDelete = "Delete";
|
||||
|
||||
function selectedDataExportTargetCount(): number {
|
||||
return sidebarTableDataExportTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds).length;
|
||||
}
|
||||
|
||||
function selectedAiTableTargets(): Array<TreeNode & { connectionId: string; database: string }> {
|
||||
return resolveSidebarDdlTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds).filter((target) => target.type === "table");
|
||||
}
|
||||
|
||||
function addToAiMenuItem(node: TreeNode): ContextMenuItem {
|
||||
const tables = node.type === "table" ? selectedAiTableTargets() : [];
|
||||
const count = tables.length;
|
||||
return {
|
||||
label: count > 1 ? t("contextMenu.addToAiMultiple", { count }) : t("contextMenu.addToAi"),
|
||||
action: () => emit("add-to-ai", count > 1 ? tables : node),
|
||||
icon: Sparkles,
|
||||
};
|
||||
}
|
||||
|
||||
function exportDataSubmenu(includeSqlInsert = true): ContextMenuItem {
|
||||
const count = selectedDataExportTargetCount();
|
||||
const children: ContextMenuItem[] = [
|
||||
{ label: "CSV", action: () => exportData("csv") },
|
||||
{ label: "JSON", action: () => exportData("json") },
|
||||
@@ -4852,7 +5013,7 @@ function exportDataSubmenu(includeSqlInsert = true): ContextMenuItem {
|
||||
if (includeSqlInsert) children.push({ label: "SQL INSERT", action: () => exportData("sql") });
|
||||
children.push({ label: "XLSX", action: () => exportDataXlsx() });
|
||||
return {
|
||||
label: t("contextMenu.exportData"),
|
||||
label: count > 1 ? t("contextMenu.exportDataMultiple", { count }) : t("contextMenu.exportData"),
|
||||
icon: Upload,
|
||||
children,
|
||||
};
|
||||
@@ -4974,7 +5135,7 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
|
||||
if (supportsQueryActions) {
|
||||
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
||||
if (supportsAiContext) {
|
||||
items.push({ label: t("contextMenu.addToAi"), action: () => emit("add-to-ai", node), icon: Sparkles });
|
||||
items.push(addToAiMenuItem(node));
|
||||
}
|
||||
}
|
||||
const connectionWorkspace = node.connectionId ? driverProfileDatabaseWorkspace(connectionStore.getConfig(node.connectionId)?.driver_profile) : undefined;
|
||||
@@ -5190,7 +5351,7 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
if (supportsConnectionQueryActions(currentDatabaseType())) {
|
||||
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
||||
if (node.type === "database" && supportsAiAssistantContext(currentDatabaseType())) {
|
||||
items.push({ label: t("contextMenu.addToAi"), action: () => emit("add-to-ai", node), icon: Sparkles });
|
||||
items.push(addToAiMenuItem(node));
|
||||
}
|
||||
const sqlHistoryMenu = savedSqlHistorySubmenu();
|
||||
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
|
||||
@@ -5524,7 +5685,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
items.push({ label: t("contextMenu.openInNewDataTab"), action: openDataInNewTabImmediately, icon: CopyPlus });
|
||||
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
||||
if (supportsAiAssistantContext(currentDatabaseType())) {
|
||||
items.push({ label: t("contextMenu.addToAi"), action: () => emit("add-to-ai", node), icon: Sparkles });
|
||||
items.push(addToAiMenuItem(node));
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push(exportDataSubmenu(false));
|
||||
@@ -5535,7 +5696,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
items.push(copyNameMenuItem());
|
||||
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
||||
if (node.type === "table" && supportsAiAssistantContext(currentDatabaseType())) {
|
||||
items.push({ label: t("contextMenu.addToAi"), action: () => emit("add-to-ai", node), icon: Sparkles });
|
||||
items.push(addToAiMenuItem(node));
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.viewData"), action: openDataImmediately, icon: TableProperties });
|
||||
@@ -6088,6 +6249,7 @@ defineExpose({
|
||||
handleRowKeydown,
|
||||
openPrimaryVisibleFilter,
|
||||
openDataInNewTab,
|
||||
openDdlForSelection,
|
||||
requestPaste,
|
||||
toggleNode,
|
||||
});
|
||||
|
||||
@@ -53,7 +53,17 @@ import { Switch } from "@/components/ui/switch";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import type { ColumnInfo, ConnectionConfig, CustomTypeTreeMemberMeta, DatabaseType, TreeNode, TriggerInfo } from "@/types/database";
|
||||
import { alignedCommentLeadingWidth, canTreeNodePin, canTreeNodeShowExpander, sidebarTreeNodeComment, trailingCommentAvailableWidth, trailingCommentGapPx, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
|
||||
import { clearActiveTableReferencePayload, createTableReferenceDragEndEvent, createTableReferenceDropEvent, createTableReferenceHoverEvent, createTableReferencePayload, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/editor/queryEditorTableDrop";
|
||||
import {
|
||||
clearActiveTableReferencePayload,
|
||||
createColumnReferencePayload,
|
||||
createMultiTableReferencePayload,
|
||||
createTableReferenceDragEndEvent,
|
||||
createTableReferenceDropEvent,
|
||||
createTableReferenceHoverEvent,
|
||||
createTableReferencePayload,
|
||||
setActiveTableReferencePayload,
|
||||
type QueryEditorTableReferencePayload,
|
||||
} from "@/lib/editor/queryEditorTableDrop";
|
||||
import { AI_ASSISTANT_TABLE_DROP_ROOT_SELECTOR } from "@/lib/ai/aiTableReferenceDrop";
|
||||
import { beginTableReferenceDragFeedback, isOverSqlEditorTarget, type TableReferenceDragFeedback } from "@/lib/editor/tableReferenceDragFeedback";
|
||||
import { formatSidebarObjectStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
|
||||
@@ -64,7 +74,8 @@ import { encodeSpannerResourcePath } from "@/lib/connection/spannerResourcePath"
|
||||
import { hexToRgba } from "@/lib/common/color";
|
||||
import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebar/sidebarLabelTooltip";
|
||||
import { filterSidebarModifierSelectionIds, supportsSidebarModifierSelection, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { filterSidebarModifierSelectionIds, selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes, supportsSidebarModifierSelection, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { resolveSidebarColumnDragNames, resolveSidebarTableCopyTargets, type SidebarTableCopyTarget } from "@/lib/sidebar/sidebarTableNameCopy";
|
||||
import { applyConnectionMultiSelection, applyTreeNodeSelection, connectionMultiSelectionAfterToggle } from "@/lib/sidebar/sidebarConnectionMultiSelect";
|
||||
import { connectionBearingGroupIdsUnder, connectionIdsUnderGroup } from "@/lib/sidebar/sidebarLayout";
|
||||
import { isSidebarDatabaseOpenForVisual } from "@/lib/sidebar/sidebarDatabaseOpenState";
|
||||
@@ -1172,13 +1183,31 @@ let referenceDragFeedback: TableReferenceDragFeedback | null = null;
|
||||
|
||||
let suppressNextTableReferenceClick = false;
|
||||
|
||||
function selectedTreeNodesInVisibleOrder(): TreeNode[] {
|
||||
return orderSelectedTreeNodes(visibleTreeNodes(), connectionStore.selectedTreeNodeIds);
|
||||
}
|
||||
|
||||
function tableReferenceDragLabel(payload: QueryEditorTableReferencePayload): string {
|
||||
if (payload.referenceType === "column" && payload.columnName) return payload.columnName;
|
||||
if (payload.referenceType === "column") {
|
||||
const names = payload.columnNames?.length ? payload.columnNames : payload.columnName ? [payload.columnName] : [];
|
||||
if (names.length <= 3) return names.join(", ");
|
||||
return t("grid.columnDragChipMany", { names: names.slice(0, 2).join(", "), count: names.length });
|
||||
}
|
||||
const tableNames = payload.tableReferences?.map((entry) => entry.tableName) ?? (payload.tableName ? [payload.tableName] : []);
|
||||
if (tableNames.length <= 3) return tableNames.join(", ");
|
||||
if (tableNames.length > 1) return t("grid.columnDragChipMany", { names: tableNames.slice(0, 2).join(", "), count: tableNames.length });
|
||||
return payload.tableName || payload.database;
|
||||
}
|
||||
|
||||
function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
if (!canDragTableReference.value) return null;
|
||||
const selectedNodes = selectedTreeNodesInVisibleOrder();
|
||||
const tableCopyOptions = {
|
||||
tableNameSeparator: settingsStore.editorSettings.sidebarCopyTableNameSeparator,
|
||||
includeTableSchema: settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema,
|
||||
databaseType: currentDatabaseType(),
|
||||
driverProfile: currentDriverProfile(),
|
||||
};
|
||||
if (activeNode.value.type === "database") {
|
||||
return createTableReferencePayload({
|
||||
connectionId: activeNode.value.connectionId,
|
||||
@@ -1189,16 +1218,24 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
});
|
||||
}
|
||||
if (activeNode.value.type === "column") {
|
||||
const columnName = columnNameForDrag(activeNode.value);
|
||||
if (!activeNode.value.tableName || !columnName) return null;
|
||||
return createTableReferencePayload({
|
||||
const columnNames = resolveSidebarColumnDragNames(activeNode.value, selectedNodes);
|
||||
if (!activeNode.value.connectionId || activeNode.value.database == null || columnNames.length === 0) return null;
|
||||
return createColumnReferencePayload({
|
||||
connectionId: activeNode.value.connectionId,
|
||||
database: activeNode.value.database,
|
||||
schema: activeNode.value.schema,
|
||||
tableName: activeNode.value.tableName,
|
||||
columnName,
|
||||
columnNames,
|
||||
databaseType: currentDatabaseType(),
|
||||
driverProfile: currentDriverProfile(),
|
||||
columnNameSeparator: settingsStore.editorSettings.sidebarCopyTableNameSeparator,
|
||||
});
|
||||
}
|
||||
const tableTargets = resolveSidebarTableCopyTargets(activeNode.value, selectedNodes);
|
||||
if (tableTargets.length > 1) {
|
||||
return createMultiTableReferencePayload({
|
||||
connectionId: activeNode.value.connectionId,
|
||||
database: activeNode.value.database,
|
||||
tableReferences: tableTargets.map((target: SidebarTableCopyTarget) => ({ schema: target.schema, tableName: target.label })),
|
||||
...tableCopyOptions,
|
||||
});
|
||||
}
|
||||
const payload = createTableReferencePayload({
|
||||
@@ -1209,15 +1246,13 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
databaseType: currentDatabaseType(),
|
||||
driverProfile: currentDriverProfile(),
|
||||
});
|
||||
if (payload && tableCopyOptions.includeTableSchema) {
|
||||
payload.includeTableSchema = true;
|
||||
payload.tableNameSeparator = tableCopyOptions.tableNameSeparator;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function columnNameForDrag(node: TreeNode): string {
|
||||
const column = node.meta as Partial<ColumnInfo> | undefined;
|
||||
if (typeof column?.name === "string" && column.name) return column.name;
|
||||
return node.label.replace(/\s+\([^()]*\)$/, "");
|
||||
}
|
||||
|
||||
function startTableReferenceDrag(payload: QueryEditorTableReferencePayload) {
|
||||
draggingTableReferencePayload = payload;
|
||||
setActiveTableReferencePayload(payload);
|
||||
|
||||
+54
@@ -45,6 +45,8 @@ vi.mock("@/stores/settingsStore", () => ({
|
||||
sidebarAllowHorizontalScroll: false,
|
||||
sidebarHiddenTablePrefixes: [],
|
||||
sidebarObjectInfoMode: "none",
|
||||
sidebarCopyTableNameSeparator: "comma",
|
||||
sidebarCopyTableNameIncludeSchema: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
@@ -147,4 +149,56 @@ describe("TreeItem table reference dragging", () => {
|
||||
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drags multiple selected columns from the same table", async () => {
|
||||
mockEditorDropTarget();
|
||||
const onDrop = listenForTableReferenceDrop();
|
||||
const columnA: TreeNode = {
|
||||
id: "column-id",
|
||||
label: "id (INTEGER)",
|
||||
type: "column",
|
||||
connectionId: "connection-1",
|
||||
database: "main",
|
||||
schema: "main",
|
||||
tableName: "orders",
|
||||
meta: { name: "id" },
|
||||
};
|
||||
const columnB: TreeNode = {
|
||||
id: "column-name",
|
||||
label: "name (TEXT)",
|
||||
type: "column",
|
||||
connectionId: "connection-1",
|
||||
database: "main",
|
||||
schema: "main",
|
||||
tableName: "orders",
|
||||
meta: { name: "name" },
|
||||
};
|
||||
connectionStore.selectedTreeNodeIds = [columnA.id, columnB.id];
|
||||
connectionStore.selectedTreeNodeIdsSet = new Set([columnA.id, columnB.id]);
|
||||
connectionStore.selectedTreeNodeId = columnA.id;
|
||||
connectionStore.treeNodes = [columnA, columnB];
|
||||
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () => h(TreeItem, { node: columnA, depth: 3 }),
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
app.use(i18n);
|
||||
app.provide(sidebarTreeRuntimeKey, createSidebarTreeRuntime());
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
|
||||
const row = container.querySelector<HTMLElement>("[tabindex]");
|
||||
if (!row) throw new Error("Tree item row was not rendered");
|
||||
dragToEditor(row);
|
||||
|
||||
expect(onDrop).toHaveBeenCalledOnce();
|
||||
const detail = (onDrop.mock.calls[0][0] as CustomEvent<QueryEditorTableReferenceDropDetail>).detail;
|
||||
expect(detail.payload.referenceType).toBe("column");
|
||||
expect(detail.payload.columnNames).toEqual(["id", "name"]);
|
||||
expect(detail.payload.columnNameSeparator).toBe("comma");
|
||||
});
|
||||
});
|
||||
+133
-5
@@ -112,7 +112,7 @@ describe("useSidebarTreeExportRuntime", () => {
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: {} as never,
|
||||
settingsStore: exportSettings() as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
@@ -156,15 +156,15 @@ describe("useSidebarTreeExportRuntime", () => {
|
||||
|
||||
expect(source).toContain('import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue"');
|
||||
expect(exportDataXlsx).toContain("await api.getColumns(");
|
||||
expect(exportDataXlsx).toContain("hasXlsxHeaderComments(columnInfos?.map((column) => column.comment))");
|
||||
expect(exportDataXlsx.indexOf("await showSidebarTreeXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(target, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)'));
|
||||
expect(exportDataXlsx).toContain("hasXlsxHeaderComments(columnInfos.map((column) => column.comment))");
|
||||
expect(exportDataXlsx.indexOf("await showSidebarTreeXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(target, "xlsx"'));
|
||||
});
|
||||
|
||||
it("falls back to field-name headers when column metadata is unavailable", () => {
|
||||
const exportDataXlsx = functionBody("exportDataXlsx");
|
||||
|
||||
expect(exportDataXlsx).toContain("catch {\n // Export still works with field-name headers when column metadata is unavailable.\n }");
|
||||
expect(exportDataXlsx).toContain('await exportTableData(target, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)');
|
||||
expect(exportDataXlsx).toContain("// Export still works with field-name headers when column metadata is unavailable.");
|
||||
expect(exportDataXlsx).toContain('await exportTableData(target, "xlsx"');
|
||||
});
|
||||
|
||||
it("sends the selected mode's header overrides to both XLSX export paths", () => {
|
||||
@@ -246,4 +246,132 @@ describe("useSidebarTreeExportRuntime", () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("exports every selected table when multiple tables are selected", async () => {
|
||||
const first = { id: "table-1", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "public", children: [] } as TreeNode;
|
||||
const second = { id: "table-2", type: "table", label: "orders", connectionId: "conn-1", database: "db", schema: "public", children: [] } as TreeNode;
|
||||
const group = { id: "tables", type: "group-tables", label: "Tables", children: [first, second] } as TreeNode;
|
||||
const activeNode = shallowRef(first);
|
||||
const settingsStore = exportSettings();
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "postgres" })),
|
||||
connectionIdentifierQuote: vi.fn(() => '"'),
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [second.id, first.id],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: settingsStore as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
await runtime.exportData("csv");
|
||||
|
||||
expect(apiMock.startTableExport).toHaveBeenCalledTimes(2);
|
||||
expect(apiMock.startTableExport).toHaveBeenNthCalledWith(1, expect.objectContaining({ tableName: "users", filePath: "users.csv" }), expect.any(Function));
|
||||
expect(apiMock.startTableExport).toHaveBeenNthCalledWith(2, expect.objectContaining({ tableName: "orders", filePath: "orders.csv" }), expect.any(Function));
|
||||
});
|
||||
|
||||
it("exports same-name tables from different schemas to distinct files", async () => {
|
||||
const publicUsers = { id: "table-1", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "public", children: [] } as TreeNode;
|
||||
const salesUsers = { id: "table-2", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "sales", children: [] } as TreeNode;
|
||||
const group = { id: "tables", type: "group-tables", label: "Tables", children: [publicUsers, salesUsers] } as TreeNode;
|
||||
const activeNode = shallowRef(publicUsers);
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "postgres" })),
|
||||
connectionIdentifierQuote: vi.fn(() => '"'),
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [salesUsers.id, publicUsers.id],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: exportSettings() as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
await runtime.exportData("csv");
|
||||
|
||||
expect(apiMock.startTableExport).toHaveBeenCalledTimes(2);
|
||||
expect(apiMock.startTableExport).toHaveBeenNthCalledWith(1, expect.objectContaining({ tableName: "users", filePath: "public.users.csv" }), expect.any(Function));
|
||||
expect(apiMock.startTableExport).toHaveBeenNthCalledWith(2, expect.objectContaining({ tableName: "users", filePath: "sales.users.csv" }), expect.any(Function));
|
||||
});
|
||||
|
||||
it("keeps unique table names unqualified when schemas differ without collisions", async () => {
|
||||
const publicUsers = { id: "table-1", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "public", children: [] } as TreeNode;
|
||||
const salesOrders = { id: "table-2", type: "table", label: "orders", connectionId: "conn-1", database: "db", schema: "sales", children: [] } as TreeNode;
|
||||
const group = { id: "tables", type: "group-tables", label: "Tables", children: [publicUsers, salesOrders] } as TreeNode;
|
||||
const activeNode = shallowRef(publicUsers);
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "postgres" })),
|
||||
connectionIdentifierQuote: vi.fn(() => '"'),
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [salesOrders.id, publicUsers.id],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: exportSettings() as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
await runtime.exportData("csv");
|
||||
|
||||
expect(apiMock.startTableExport).toHaveBeenCalledTimes(2);
|
||||
expect(apiMock.startTableExport).toHaveBeenNthCalledWith(1, expect.objectContaining({ filePath: "users.csv" }), expect.any(Function));
|
||||
expect(apiMock.startTableExport).toHaveBeenNthCalledWith(2, expect.objectContaining({ filePath: "orders.csv" }), expect.any(Function));
|
||||
});
|
||||
|
||||
it("exports only tables in the active execution context when the selection spans connections", async () => {
|
||||
const localUsers = { id: "table-1", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "public", children: [] } as TreeNode;
|
||||
const foreignOrders = { id: "table-2", type: "table", label: "orders", connectionId: "conn-2", database: "db", schema: "public", children: [] } as TreeNode;
|
||||
const foreignDatabase = { id: "db-b", type: "database", label: "db", connectionId: "conn-2", children: [foreignOrders] } as TreeNode;
|
||||
const localGroup = { id: "tables", type: "group-tables", label: "Tables", children: [localUsers] } as TreeNode;
|
||||
const localDatabase = { id: "db-a", type: "database", label: "db", connectionId: "conn-1", children: [localGroup] } as TreeNode;
|
||||
const activeNode = shallowRef(localUsers);
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "postgres" })),
|
||||
connectionIdentifierQuote: vi.fn(() => '"'),
|
||||
treeNodes: [localDatabase, foreignDatabase],
|
||||
selectedTreeNodeIds: [foreignOrders.id, localUsers.id],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: exportSettings() as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
await runtime.exportData("csv");
|
||||
|
||||
expect(apiMock.startTableExport).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.startTableExport).toHaveBeenCalledWith(expect.objectContaining({ connectionId: "conn-1", tableName: "users", filePath: "users.csv" }), expect.any(Function));
|
||||
});
|
||||
|
||||
it("keeps single-table export behavior when only one table is selected", async () => {
|
||||
const activeNode = shallowRef({ id: "table-1", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "public", children: [] } as TreeNode);
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "postgres" })),
|
||||
connectionIdentifierQuote: vi.fn(() => '"'),
|
||||
treeNodes: [],
|
||||
selectedTreeNodeIds: [],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: exportSettings() as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
await runtime.exportData("csv");
|
||||
|
||||
expect(apiMock.startTableExport).toHaveBeenCalledOnce();
|
||||
expect(apiMock.startTableExport).toHaveBeenCalledWith(expect.objectContaining({ tableName: "users", filePath: "users.csv" }), expect.any(Function));
|
||||
});
|
||||
});
|
||||
+66
-2
@@ -4,15 +4,22 @@ import type { TreeNode } from "@/types/database";
|
||||
|
||||
import { useSidebarTreeToolRuntime } from "@/composables/useSidebarTreeToolRuntime";
|
||||
|
||||
function setup(node: Partial<TreeNode>) {
|
||||
function setup(node: Partial<TreeNode>, options: { treeNodes?: TreeNode[]; selectedTreeNodeIds?: string[]; acceptedSelectionIds?: readonly string[] | null } = {}) {
|
||||
const activeNode = shallowRef({ id: "n-1", label: "node", children: [], ...node } as TreeNode);
|
||||
const connectionStore = { docsSource: null as unknown };
|
||||
const connectionStore = {
|
||||
docsSource: null as unknown,
|
||||
diagramSource: null as unknown,
|
||||
databaseExportSource: null as unknown,
|
||||
treeNodes: options.treeNodes ?? [],
|
||||
selectedTreeNodeIds: options.selectedTreeNodeIds ?? [],
|
||||
};
|
||||
const runtime = useSidebarTreeToolRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
queryStore: {} as never,
|
||||
settingsStore: {} as never,
|
||||
tableChildObjectName: () => "",
|
||||
acceptedSelectionIds: () => options.acceptedSelectionIds ?? null,
|
||||
});
|
||||
return { connectionStore, runtime };
|
||||
}
|
||||
@@ -43,3 +50,60 @@ describe("useSidebarTreeToolRuntime openDocs", () => {
|
||||
expect(connectionStore.docsSource).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useSidebarTreeToolRuntime diagram and database export", () => {
|
||||
const publicUsers: TreeNode = { id: "t1", label: "users", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const publicOrders: TreeNode = { id: "t2", label: "orders", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const salesUsers: TreeNode = { id: "t3", label: "users", type: "table", connectionId: "c1", database: "db", schema: "sales" };
|
||||
const publicView: TreeNode = { id: "v1", label: "active_users", type: "view", connectionId: "c1", database: "db", schema: "public" };
|
||||
const group: TreeNode = { id: "group", label: "Tables", type: "group-tables", children: [publicUsers, publicOrders, salesUsers, publicView] };
|
||||
|
||||
it("opens a multi-table diagram only for tables in the active schema", () => {
|
||||
const { connectionStore, runtime } = setup(publicUsers, {
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [publicOrders.id, publicUsers.id, salesUsers.id],
|
||||
});
|
||||
|
||||
runtime.openDiagram();
|
||||
|
||||
expect(connectionStore.diagramSource).toEqual({
|
||||
connectionId: "c1",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableNames: ["users", "orders"],
|
||||
});
|
||||
});
|
||||
|
||||
it("prefills database export with same-schema tables only", () => {
|
||||
const { connectionStore, runtime } = setup(publicUsers, {
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [publicOrders.id, publicUsers.id, publicView.id],
|
||||
});
|
||||
|
||||
runtime.openDatabaseExport();
|
||||
|
||||
expect(connectionStore.databaseExportSource).toEqual({
|
||||
connectionId: "c1",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
tableNames: ["users", "orders"],
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to single-table database export when the selection spans schemas", () => {
|
||||
const { connectionStore, runtime } = setup(publicUsers, {
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [salesUsers.id, publicUsers.id],
|
||||
});
|
||||
|
||||
runtime.openDatabaseExport();
|
||||
|
||||
expect(connectionStore.databaseExportSource).toEqual({
|
||||
connectionId: "c1",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,7 @@ const diagramPrefillConnectionId = ref("");
|
||||
const diagramPrefillDatabase = ref("");
|
||||
const diagramPrefillSchema = ref("");
|
||||
const diagramFocusTableName = ref("");
|
||||
const diagramFocusTableNames = ref<string[]>([]);
|
||||
const docsPrefillConnectionId = ref("");
|
||||
const docsPrefillDatabase = ref("");
|
||||
const docsPrefillSchema = ref("");
|
||||
@@ -183,6 +184,7 @@ export function useDialogSources() {
|
||||
diagramPrefillDatabase.value = v.database;
|
||||
diagramPrefillSchema.value = v.schema ?? "";
|
||||
diagramFocusTableName.value = v.tableName ?? "";
|
||||
diagramFocusTableNames.value = v.tableNames ?? (v.tableName ? [v.tableName] : []);
|
||||
showDiagramDialog.value = true;
|
||||
connectionStore.diagramSource = null;
|
||||
}
|
||||
@@ -511,6 +513,7 @@ export function useDialogSources() {
|
||||
diagramPrefillDatabase,
|
||||
diagramPrefillSchema,
|
||||
diagramFocusTableName,
|
||||
diagramFocusTableNames,
|
||||
docsPrefillConnectionId,
|
||||
docsPrefillDatabase,
|
||||
docsPrefillSchema,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
|
||||
import { joinExportedDdls } from "@/lib/export/ddlExport";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { sidebarStructureExportTargets, sidebarTableDataExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
@@ -29,6 +29,7 @@ interface SidebarTreeExportRuntimeOptions {
|
||||
}
|
||||
|
||||
interface SidebarTableExportTarget {
|
||||
nodeId: string;
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
@@ -41,6 +42,44 @@ interface SidebarTableExportTarget {
|
||||
identifierQuote?: string;
|
||||
batchSize: number;
|
||||
rowLimit: number | null;
|
||||
fileNameBase?: string;
|
||||
}
|
||||
|
||||
function tableExportTargetCacheKey(target: Pick<SidebarTableExportTarget, "nodeId">): string {
|
||||
return target.nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch exports share one output directory, so same-name tables from different
|
||||
* schemas would silently overwrite each other. Qualify colliding names with
|
||||
* their schema and fall back to numeric suffixes for remaining collisions.
|
||||
*/
|
||||
function applyTableExportFileBaseNames(targets: readonly SidebarTableExportTarget[]): void {
|
||||
const tableNameCounts = new Map<string, number>();
|
||||
for (const target of targets) tableNameCounts.set(target.tableName, (tableNameCounts.get(target.tableName) ?? 0) + 1);
|
||||
|
||||
const usedNames = new Set<string>();
|
||||
for (const target of targets) {
|
||||
const base = (tableNameCounts.get(target.tableName) ?? 0) > 1 && target.schema ? `${target.schema}.${target.tableName}` : target.tableName;
|
||||
let name = base;
|
||||
let suffix = 2;
|
||||
while (usedNames.has(name)) name = `${base}-${suffix++}`;
|
||||
usedNames.add(name);
|
||||
target.fileNameBase = name;
|
||||
}
|
||||
}
|
||||
|
||||
interface ExportTableDataOptions {
|
||||
columnInfos?: ColumnInfo[];
|
||||
headerMode?: XlsxHeaderMode;
|
||||
autoFilter?: boolean;
|
||||
outputDirectory?: string;
|
||||
suppressDoneToast?: boolean;
|
||||
}
|
||||
|
||||
function joinExportFilePath(directory: string, fileName: string): string {
|
||||
const separator = directory.includes("\\") ? "\\" : "/";
|
||||
return directory.endsWith(separator) ? `${directory}${fileName}` : `${directory}${separator}${fileName}`;
|
||||
}
|
||||
|
||||
export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOptions) {
|
||||
@@ -80,6 +119,10 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
return sidebarStructureExportTargets(activeNode.value, connectionStore.treeNodes, options.acceptedSelectionIds() ?? connectionStore.selectedTreeNodeIds);
|
||||
}
|
||||
|
||||
function tableDataExportTargets(): Array<TreeNode & { connectionId: string; database: string }> {
|
||||
return sidebarTableDataExportTargets(activeNode.value, connectionStore.treeNodes, options.acceptedSelectionIds() ?? connectionStore.selectedTreeNodeIds);
|
||||
}
|
||||
|
||||
async function exportStructure() {
|
||||
const targets = structureExportTargets();
|
||||
if (!targets.length) return;
|
||||
@@ -217,41 +260,56 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
}
|
||||
}
|
||||
|
||||
async function exportDataLegacy(format: "json") {
|
||||
const node = activeNode.value;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
const connectionId = node.connectionId;
|
||||
const database = node.database;
|
||||
async function pickTableExportDirectory(): Promise<string | null> {
|
||||
if (!isTauriRuntime()) return "";
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({ directory: true, multiple: false, title: t("contextMenu.exportDataSelectDirectory") });
|
||||
return typeof selected === "string" ? selected : null;
|
||||
}
|
||||
|
||||
async function resolveTableExportOutputPath(target: SidebarTableExportTarget, format: string, outputDirectory?: string): Promise<string | null> {
|
||||
const fileName = `${target.fileNameBase ?? target.tableName}.${format}`;
|
||||
if (outputDirectory !== undefined) {
|
||||
return outputDirectory ? joinExportFilePath(outputDirectory, fileName) : fileName;
|
||||
}
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const filterName = format === "csv" ? "CSV" : format === "json" ? "JSON" : format === "xlsx" ? "Excel" : "SQL";
|
||||
const path = await save({
|
||||
defaultPath: fileName,
|
||||
filters: [{ name: filterName, extensions: [format] }],
|
||||
});
|
||||
return path ? String(path) : null;
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
async function exportDataLegacyForTarget(target: SidebarTableExportTarget, outputDirectory?: string, suppressDoneToast = false) {
|
||||
const { connectionId, database } = target;
|
||||
const config = connectionStore.getConfig(connectionId);
|
||||
if (!config) return;
|
||||
if (!config) return false;
|
||||
|
||||
try {
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
const queryColumns = config.db_type === "neo4j" ? (await api.getColumns(connectionId, database, node.schema || database, node.label)).map((column) => column.name) : undefined;
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
|
||||
const queryColumns = config.db_type === "neo4j" ? (await api.getColumns(connectionId, database, target.metadataSchema, target.tableName, target.catalog)).map((column) => column.name) : undefined;
|
||||
const result = await fetchTableDataForExport({
|
||||
databaseType: effectiveDbType,
|
||||
identifierQuote: connectionStore.connectionIdentifierQuote?.(connectionId),
|
||||
schema: node.schema,
|
||||
tableName: node.label,
|
||||
tableType: node.tableType,
|
||||
databaseType: target.databaseType,
|
||||
identifierQuote: target.identifierQuote,
|
||||
schema: target.schema,
|
||||
tableName: target.tableName,
|
||||
tableType: target.tableType,
|
||||
columns: queryColumns,
|
||||
executePage: (sql) => api.executeQuery(connectionId, database, sql),
|
||||
});
|
||||
|
||||
if (format === "json") {
|
||||
let outputPath = `${node.label}.json`;
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await save({ defaultPath: outputPath, filters: [{ name: "JSON", extensions: ["json"] }] });
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
}
|
||||
const outputPath = await resolveTableExportOutputPath(target, "json", outputDirectory);
|
||||
if (!outputPath) return false;
|
||||
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
|
||||
if (!suppressDoneToast) toast(t("grid.exported"));
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,8 +338,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
});
|
||||
}
|
||||
|
||||
function currentTableExportTarget(): SidebarTableExportTarget | null {
|
||||
const node = activeNode.value;
|
||||
function tableExportTargetFromNode(node: TreeNode): SidebarTableExportTarget | null {
|
||||
if (!node.connectionId || !node.database) return null;
|
||||
const connectionId = node.connectionId;
|
||||
const database = node.database;
|
||||
@@ -290,6 +347,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
const editorSettings = settingsStore.editorSettings;
|
||||
|
||||
return {
|
||||
nodeId: node.id,
|
||||
connectionId,
|
||||
database,
|
||||
schema: node.schema || undefined,
|
||||
@@ -305,23 +363,22 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
};
|
||||
}
|
||||
|
||||
async function exportTableData(target: SidebarTableExportTarget, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name", autoFilter = true) {
|
||||
function currentTableExportTargets(): SidebarTableExportTarget[] {
|
||||
const targets = tableDataExportTargets()
|
||||
.map((node) => tableExportTargetFromNode(node))
|
||||
.filter((target): target is SidebarTableExportTarget => target != null);
|
||||
applyTableExportFileBaseNames(targets);
|
||||
return targets;
|
||||
}
|
||||
|
||||
async function exportTableData(target: SidebarTableExportTarget, format: "csv" | "xlsx" | "sql", exportOptions: ExportTableDataOptions = {}) {
|
||||
const { columnInfos, headerMode = "name", autoFilter = true, outputDirectory, suppressDoneToast = false } = exportOptions;
|
||||
const { connectionId, database } = target;
|
||||
|
||||
let task: ExportTask | null = null;
|
||||
try {
|
||||
// Choose the destination before registering a background task so cancellation creates no orphan tracker entry.
|
||||
let outputPath = `${target.tableName}.${format}`;
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const filterName = format === "csv" ? "CSV" : format === "xlsx" ? "Excel" : "SQL";
|
||||
const path = await save({
|
||||
defaultPath: outputPath,
|
||||
filters: [{ name: filterName, extensions: [format] }],
|
||||
});
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const outputPath = await resolveTableExportOutputPath(target, format, outputDirectory);
|
||||
if (!outputPath) return false;
|
||||
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
task = addExportTask(target.tableName, format, outputPath);
|
||||
@@ -347,8 +404,8 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
currentTask.status = "Done";
|
||||
currentTask.rowsExported = result.rows.length;
|
||||
currentTask.totalRows = result.rows.length;
|
||||
toast(t("grid.exported"));
|
||||
return;
|
||||
if (!suppressDoneToast) toast(t("grid.exported"));
|
||||
return true;
|
||||
}
|
||||
const columnComments =
|
||||
format === "xlsx" && exportColumnInfos
|
||||
@@ -378,41 +435,89 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
|
||||
await api.startTableExport(request, (progress) => {
|
||||
updateTableExportTask(currentTask.exportId, progress);
|
||||
if (progress.status === "Done") toast(t("grid.exported"));
|
||||
else if (progress.status === "Error") toast(t("grid.exportFailed", { message: translateBackendError(t, progress.errorMessage || "") }), 5000);
|
||||
if (progress.status === "Done") {
|
||||
if (!suppressDoneToast) toast(t("grid.exported"));
|
||||
} else if (progress.status === "Error") toast(t("grid.exportFailed", { message: translateBackendError(t, progress.errorMessage || "") }), 5000);
|
||||
});
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (task) {
|
||||
task.status = "Error";
|
||||
task.errorMessage = error?.message || String(error);
|
||||
}
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveMultiTableExportDirectory(targetCount: number): Promise<string | undefined | null> {
|
||||
if (targetCount <= 1) return undefined;
|
||||
if (!isTauriRuntime()) return "";
|
||||
return pickTableExportDirectory();
|
||||
}
|
||||
|
||||
async function exportData(format: "csv" | "json" | "sql") {
|
||||
if (format === "json") await exportDataLegacy(format);
|
||||
else {
|
||||
const target = currentTableExportTarget();
|
||||
if (target) await exportTableData(target, format);
|
||||
const targets = currentTableExportTargets();
|
||||
if (!targets.length) return;
|
||||
|
||||
const outputDirectory = await resolveMultiTableExportDirectory(targets.length);
|
||||
if (outputDirectory === null) return;
|
||||
|
||||
if (format === "json") {
|
||||
let exported = 0;
|
||||
for (const target of targets) {
|
||||
if (await exportDataLegacyForTarget(target, outputDirectory, targets.length > 1)) exported += 1;
|
||||
}
|
||||
if (targets.length > 1 && exported > 0) toast(t("contextMenu.exportDataMultipleSuccess", { count: exported }));
|
||||
return;
|
||||
}
|
||||
|
||||
let exported = 0;
|
||||
for (const target of targets) {
|
||||
if (await exportTableData(target, format, { outputDirectory, suppressDoneToast: targets.length > 1 })) exported += 1;
|
||||
}
|
||||
if (targets.length > 1 && exported > 0) toast(t("contextMenu.exportDataMultipleSuccess", { count: exported }));
|
||||
}
|
||||
|
||||
async function exportDataXlsx() {
|
||||
const target = currentTableExportTarget();
|
||||
if (!target) return;
|
||||
const targets = currentTableExportTargets();
|
||||
if (!targets.length) return;
|
||||
|
||||
let columnInfos: ColumnInfo[] | undefined;
|
||||
try {
|
||||
await connectionStore.ensureConnected(target.connectionId);
|
||||
columnInfos = await api.getColumns(target.connectionId, target.database, target.metadataSchema, target.tableName, target.catalog);
|
||||
} catch {
|
||||
// Export still works with field-name headers when column metadata is unavailable.
|
||||
const columnInfosByTarget = new Map<string, ColumnInfo[] | undefined>();
|
||||
let hasComments = false;
|
||||
for (const target of targets) {
|
||||
let columnInfos: ColumnInfo[] | undefined;
|
||||
try {
|
||||
await connectionStore.ensureConnected(target.connectionId);
|
||||
columnInfos = await api.getColumns(target.connectionId, target.database, target.metadataSchema, target.tableName, target.catalog);
|
||||
if (hasXlsxHeaderComments(columnInfos.map((column) => column.comment))) hasComments = true;
|
||||
} catch {
|
||||
// Export still works with field-name headers when column metadata is unavailable.
|
||||
}
|
||||
columnInfosByTarget.set(tableExportTargetCacheKey(target), columnInfos);
|
||||
}
|
||||
|
||||
const exportOptions = await showSidebarTreeXlsxHeaderDialog(hasXlsxHeaderComments(columnInfos?.map((column) => column.comment)));
|
||||
if (exportOptions === null) return;
|
||||
await exportTableData(target, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter);
|
||||
const xlsxOptions = await showSidebarTreeXlsxHeaderDialog(hasComments);
|
||||
if (xlsxOptions === null) return;
|
||||
|
||||
const outputDirectory = await resolveMultiTableExportDirectory(targets.length);
|
||||
if (outputDirectory === null) return;
|
||||
|
||||
let exported = 0;
|
||||
for (const target of targets) {
|
||||
if (
|
||||
await exportTableData(target, "xlsx", {
|
||||
columnInfos: columnInfosByTarget.get(tableExportTargetCacheKey(target)),
|
||||
headerMode: xlsxOptions.headerMode,
|
||||
autoFilter: xlsxOptions.autoFilter,
|
||||
outputDirectory,
|
||||
suppressDoneToast: targets.length > 1,
|
||||
})
|
||||
) {
|
||||
exported += 1;
|
||||
}
|
||||
}
|
||||
if (targets.length > 1 && exported > 0) toast(t("contextMenu.exportDataMultipleSuccess", { count: exported }));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { useConnectionStore } from "@/stores/connectionStore";
|
||||
import type { useQueryStore } from "@/stores/queryStore";
|
||||
import type { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { allDatabasesExportSourceForNode, databaseExportSourceForNode } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { allDatabasesExportSourceForNode, databaseExportSourceForNode, sidebarSameSchemaStructureTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
|
||||
interface SidebarTreeToolRuntimeOptions {
|
||||
activeNode: ShallowRef<TreeNode>;
|
||||
@@ -11,6 +11,7 @@ interface SidebarTreeToolRuntimeOptions {
|
||||
queryStore: ReturnType<typeof useQueryStore>;
|
||||
settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
tableChildObjectName: (node: TreeNode) => string;
|
||||
acceptedSelectionIds?: () => readonly string[] | null;
|
||||
}
|
||||
|
||||
export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions) {
|
||||
@@ -52,14 +53,21 @@ export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions
|
||||
};
|
||||
}
|
||||
|
||||
function selectedSameSchemaStructureTargets() {
|
||||
return sidebarSameSchemaStructureTargets(activeNode.value, connectionStore.treeNodes, options.acceptedSelectionIds?.() ?? connectionStore.selectedTreeNodeIds);
|
||||
}
|
||||
|
||||
function openDiagram() {
|
||||
const node = activeNode.value;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
const tables = selectedSameSchemaStructureTargets().filter((target) => target.type === "table");
|
||||
const tableNames = tables.length > 1 ? tables.map((target) => target.label) : undefined;
|
||||
connectionStore.diagramSource = {
|
||||
connectionId: node.connectionId,
|
||||
database: node.database,
|
||||
schema: node.schema,
|
||||
tableName: node.type === "table" ? node.label : undefined,
|
||||
tableName: tableNames?.length ? tableNames[0] : node.type === "table" ? node.label : undefined,
|
||||
tableNames,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,7 +94,18 @@ export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions
|
||||
}
|
||||
|
||||
function openDatabaseExport() {
|
||||
connectionStore.databaseExportSource = databaseExportSourceForNode(activeNode.value);
|
||||
const node = activeNode.value;
|
||||
const tables = selectedSameSchemaStructureTargets().filter((target) => target.type === "table");
|
||||
if (tables.length > 1 && node.connectionId && node.database) {
|
||||
connectionStore.databaseExportSource = {
|
||||
connectionId: node.connectionId,
|
||||
database: node.database,
|
||||
schema: node.schema,
|
||||
tableNames: tables.map((target) => target.label),
|
||||
};
|
||||
return;
|
||||
}
|
||||
connectionStore.databaseExportSource = databaseExportSourceForNode(node);
|
||||
}
|
||||
|
||||
function openAllDatabasesExport() {
|
||||
|
||||
@@ -2945,6 +2945,7 @@ export default {
|
||||
duplicateSelectedConnections: "Duplicate Selected {count} Connections",
|
||||
newQuery: "New Query",
|
||||
addToAi: "Add to AI",
|
||||
addToAiMultiple: "Add to AI ({count} tables)",
|
||||
instanceInfo: "Instance Info",
|
||||
newSql: "New SQL",
|
||||
generateSql: "Generate SQL",
|
||||
@@ -3011,6 +3012,9 @@ export default {
|
||||
restoreSqliteDatabaseFailed: "Failed to restore SQLite database: {message}",
|
||||
importData: "Import Data",
|
||||
exportData: "Export Data",
|
||||
exportDataMultiple: "Export Data ({count} tables)",
|
||||
exportDataSelectDirectory: "Select export folder",
|
||||
exportDataMultipleSuccess: "Exported {count} tables",
|
||||
exportStructure: "Export Structure",
|
||||
exportStructurePreviewTitle: "Structure - {name}",
|
||||
exportStructurePreviewTitleMultiple: "Structure - {count} objects",
|
||||
@@ -6515,6 +6519,10 @@ export default {
|
||||
sidebarHiddenTablePrefixes: "Hidden table name prefixes",
|
||||
sidebarHiddenTablePrefixesDescription: "One prefix per line. Only sidebar table, view, and collection labels are shortened; tooltips and actions still use the full name.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Example:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "Copy table name separator",
|
||||
sidebarCopyTableNameSeparatorDescription: "Separator used when copying or dragging multiple sidebar table or column names into the SQL editor.",
|
||||
sidebarCopyTableNameIncludeSchema: "Include schema in copied table names",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "When enabled, copied or dragged table names use schema-qualified form with the active connection's identifier quoting rules.",
|
||||
sidebarTablePageSize: "Sidebar page size",
|
||||
sidebarTablePageSizeDescription: "Maximum number of tables/objects loaded per page in the sidebar tree. Increase if you have many tables and want fewer pages.",
|
||||
sidebarTableSearchEnabled: "Enable table search inside databases",
|
||||
|
||||
@@ -2873,6 +2873,7 @@ export default withEnglishFallback({
|
||||
instanceInfo: "Información de la instancia",
|
||||
newQuery: "Nueva consulta",
|
||||
addToAi: "Agregar a la IA",
|
||||
addToAiMultiple: "Agregar a la IA ({count} tablas)",
|
||||
newSql: "Nuevo SQL",
|
||||
generateSql: "Generar SQL",
|
||||
newInsert: "Nueva Inserción",
|
||||
@@ -2936,6 +2937,9 @@ export default withEnglishFallback({
|
||||
restoreSqliteDatabaseFailed: "Error al restaurar la base de datos SQLite: {message}",
|
||||
importData: "Importar datos",
|
||||
exportData: "Exportar datos",
|
||||
exportDataMultiple: "Exportar datos ({count} tablas)",
|
||||
exportDataSelectDirectory: "Seleccionar carpeta de exportación",
|
||||
exportDataMultipleSuccess: "Se exportaron {count} tablas",
|
||||
exportStructure: "Exportar estructura",
|
||||
exportStructurePreviewTitle: "Estructura - {name}",
|
||||
exportStructurePreviewTitleMultiple: "Estructura - {count} objetos",
|
||||
@@ -6186,6 +6190,10 @@ export default withEnglishFallback({
|
||||
sidebarHiddenTablePrefixes: "Prefijos ocultos de tablas",
|
||||
sidebarHiddenTablePrefixesDescription: "Un prefijo por linea. Solo acorta etiquetas de tablas, vistas y colecciones en la barra lateral; las acciones y ayudas usan el nombre completo.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Ejemplo:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "Separador para copiar nombres de tabla",
|
||||
sidebarCopyTableNameSeparatorDescription: "Separador usado al copiar o arrastrar varios nombres de tablas o columnas de la barra lateral al editor SQL.",
|
||||
sidebarCopyTableNameIncludeSchema: "Incluir esquema en nombres de tabla copiados",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "Si está activado, los nombres copiados o arrastrados usan la forma esquema.tabla con las reglas de comillas del identificador de la conexión activa.",
|
||||
sidebarTablePageSize: "Tamaño de página de la barra lateral",
|
||||
sidebarTablePageSizeDescription: "Número máximo de tablas/objetos cargados por página en el árbol lateral. Auméntalo si tienes muchas tablas para reducir páginas.",
|
||||
sidebarTableSearchEnabled: "Activar búsqueda de tablas por base",
|
||||
|
||||
@@ -2870,6 +2870,7 @@ export default withEnglishFallback({
|
||||
duplicateSelectedConnections: "Duplica {count} connessioni selezionate",
|
||||
newQuery: "Nuova Query",
|
||||
addToAi: "Aggiungi all'IA",
|
||||
addToAiMultiple: "Aggiungi all'IA ({count} tabelle)",
|
||||
instanceInfo: "Informazioni Istanza",
|
||||
newSql: "Nuovo SQL",
|
||||
generateSql: "Genera SQL",
|
||||
@@ -2934,6 +2935,9 @@ export default withEnglishFallback({
|
||||
restoreSqliteDatabaseFailed: "Impossibile ripristinare il database SQLite: {message}",
|
||||
importData: "Importa Dati",
|
||||
exportData: "Esporta Dati",
|
||||
exportDataMultiple: "Esporta dati ({count} tabelle)",
|
||||
exportDataSelectDirectory: "Seleziona cartella di esportazione",
|
||||
exportDataMultipleSuccess: "Esportate {count} tabelle",
|
||||
exportStructure: "Esporta Struttura",
|
||||
exportStructurePreviewTitle: "Struttura - {name}",
|
||||
exportStructurePreviewTitleMultiple: "Struttura - {count} oggetti",
|
||||
@@ -6186,6 +6190,10 @@ export default withEnglishFallback({
|
||||
sidebarHiddenTablePrefixes: "Prefissi dei nomi delle tabelle nascosti",
|
||||
sidebarHiddenTablePrefixesDescription: "Un prefisso per riga. Solo le etichette di tabelle, viste e collezioni della barra laterale vengono abbreviate; i suggerimenti e le azioni utilizzano ancora il nome completo.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Esempio:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "Separatore per copiare i nomi delle tabelle",
|
||||
sidebarCopyTableNameSeparatorDescription: "Separatore usato quando si copiano o trascinano più nomi di tabelle o colonne dalla barra laterale nell'editor SQL.",
|
||||
sidebarCopyTableNameIncludeSchema: "Includi lo schema nei nomi di tabella copiati",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "Se attivato, i nomi copiati o trascinati usano la forma schema.tabella con le regole di quoting dell'identificatore della connessione attiva.",
|
||||
sidebarTablePageSize: "Dimensione pagina barra laterale",
|
||||
sidebarTablePageSizeDescription: "Numero massimo di tabelle/oggetti caricati per pagina nell'albero laterale. Aumentalo se hai molte tabelle per ridurre le pagine.",
|
||||
sidebarTableSearchEnabled: "Abilita ricerca tabelle nel database",
|
||||
|
||||
@@ -2895,6 +2895,7 @@ export default withEnglishFallback({
|
||||
duplicateSelectedConnections: "選択した{count}件の接続を複製",
|
||||
newQuery: "新しいクエリ",
|
||||
addToAi: "AI に追加",
|
||||
addToAiMultiple: "AI に追加({count} テーブル)",
|
||||
newSql: "新しいSQL",
|
||||
generateSql: "SQLを生成",
|
||||
newInsert: "新しいINSERT",
|
||||
@@ -2958,6 +2959,9 @@ export default withEnglishFallback({
|
||||
restoreSqliteDatabaseFailed: "SQLite データベースの復元に失敗しました: {message}",
|
||||
importData: "データをインポート",
|
||||
exportData: "データをエクスポート",
|
||||
exportDataMultiple: "データをエクスポート({count} テーブル)",
|
||||
exportDataSelectDirectory: "エクスポート先フォルダを選択",
|
||||
exportDataMultipleSuccess: "{count} テーブルをエクスポートしました",
|
||||
exportStructure: "構造をエクスポート",
|
||||
exportStructurePreviewTitle: "構造 - {name}",
|
||||
exportStructurePreviewTitleMultiple: "構造 - {count}オブジェクト",
|
||||
@@ -6212,6 +6216,10 @@ export default withEnglishFallback({
|
||||
sidebarHiddenTablePrefixes: "非表示テーブル名プレフィックス",
|
||||
sidebarHiddenTablePrefixesDescription: "1行に1つのプレフィックス。サイドバーのテーブル、ビュー、コレクションラベルのみ短縮されます。ツールチップと操作は完全な名前を使用します。",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Example:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "コピー時のテーブル名区切り",
|
||||
sidebarCopyTableNameSeparatorDescription: "サイドバーで複数のテーブル名または列名をコピー、または SQL エディターにドラッグするときに使う区切り文字。",
|
||||
sidebarCopyTableNameIncludeSchema: "コピーするテーブル名にスキーマを含める",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "有効にすると、コピーまたはドラッグしたテーブル名は、アクティブな接続の識別子クォート規則に従った schema.table 形式になります。",
|
||||
sidebarTablePageSize: "サイドバーのページサイズ",
|
||||
sidebarTablePageSizeDescription: "サイドバーツリーで1ページあたりに読み込むテーブル/オブジェクトの最大数です。テーブルが多い場合は増やすとページ切り替えが減ります。",
|
||||
sidebarTableSearchEnabled: "データベース内のテーブル検索を有効化",
|
||||
|
||||
@@ -2764,6 +2764,7 @@ export default withEnglishFallback({
|
||||
duplicateSelectedConnections: "선택한 연결 {count}개 복제",
|
||||
newQuery: "새 쿼리",
|
||||
addToAi: "AI에 추가",
|
||||
addToAiMultiple: "AI에 추가({count}개 테이블)",
|
||||
instanceInfo: "인스턴스 정보",
|
||||
newSql: "새 SQL",
|
||||
generateSql: "SQL 생성",
|
||||
@@ -2830,6 +2831,9 @@ export default withEnglishFallback({
|
||||
restoreSqliteDatabaseFailed: "SQLite 데이터베이스 복원 실패: {message}",
|
||||
importData: "데이터 가져오기",
|
||||
exportData: "데이터 내보내기",
|
||||
exportDataMultiple: "데이터 내보내기 ({count}개 테이블)",
|
||||
exportDataSelectDirectory: "보낼 폴더 선택",
|
||||
exportDataMultipleSuccess: "{count}개 테이블을보냈습니다",
|
||||
exportStructure: "구조 내보내기",
|
||||
exportStructurePreviewTitle: "구조 - {name}",
|
||||
exportStructurePreviewTitleMultiple: "구조 - 객체 {count}개",
|
||||
@@ -5908,6 +5912,10 @@ export default withEnglishFallback({
|
||||
sidebarHiddenTablePrefixes: "숨겨진 테이블 이름 접두사",
|
||||
sidebarHiddenTablePrefixesDescription: "한 줄에 하나의 접두사. 사이드바의 테이블, 뷰, 컬렉션 라벨만 줄이며 툴팁과 작업은 전체 이름을 계속 사용합니다.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "예:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "테이블 이름 복사 구분자",
|
||||
sidebarCopyTableNameSeparatorDescription: "사이드바에서 여러 테이블 또는 열 이름을 복사하거나 SQL 편집기로 끌어다 놓을 때 사용하는 구분자입니다.",
|
||||
sidebarCopyTableNameIncludeSchema: "복사한 테이블 이름에 스키마 포함",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "활성화하면 복사하거나 끌어다 놓은 테이블 이름이 활성 연결의 식별자 따옴표 규칙을 사용하는 스키마.테이블 형식이 됩니다.",
|
||||
sidebarTablePageSize: "사이드바 페이지 크기",
|
||||
sidebarTablePageSizeDescription: "사이드바 트리에서 페이지당 로드되는 최대 테이블/객체 수입니다. 테이블이 많고 페이지를 줄이려면 늘리세요.",
|
||||
sidebarTableSearchEnabled: "데이터베이스 내 테이블 검색 활성화",
|
||||
|
||||
@@ -2873,6 +2873,7 @@ export default withEnglishFallback({
|
||||
instanceInfo: "Informações da Instância",
|
||||
newQuery: "Nova Consulta",
|
||||
addToAi: "Adicionar à IA",
|
||||
addToAiMultiple: "Adicionar à IA ({count} tabelas)",
|
||||
newSql: "Novo SQL",
|
||||
generateSql: "Gerar SQL",
|
||||
newInsert: "Novo Insert",
|
||||
@@ -2936,6 +2937,9 @@ export default withEnglishFallback({
|
||||
restoreSqliteDatabaseFailed: "Falha ao restaurar o banco de dados SQLite: {message}",
|
||||
importData: "Importar Dados",
|
||||
exportData: "Exportar Dados",
|
||||
exportDataMultiple: "Exportar dados ({count} tabelas)",
|
||||
exportDataSelectDirectory: "Selecionar pasta de exportação",
|
||||
exportDataMultipleSuccess: "Exportadas {count} tabelas",
|
||||
exportStructure: "Exportar Estrutura",
|
||||
exportStructurePreviewTitle: "Estrutura - {name}",
|
||||
exportStructurePreviewTitleMultiple: "Estrutura - {count} objetos",
|
||||
@@ -6188,6 +6192,10 @@ export default withEnglishFallback({
|
||||
sidebarHiddenTablePrefixes: "Prefixos de nome de tabela ocultos",
|
||||
sidebarHiddenTablePrefixesDescription: "Um prefixo por linha. Apenas os rótulos de tabela, view e coleção da barra lateral são encurtados; tooltips e ações ainda usam o nome completo.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Exemplo:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "Separador para copiar nomes de tabela",
|
||||
sidebarCopyTableNameSeparatorDescription: "Separador usado ao copiar ou arrastar vários nomes de tabelas ou colunas da barra lateral para o editor SQL.",
|
||||
sidebarCopyTableNameIncludeSchema: "Incluir schema nos nomes de tabela copiados",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "Quando ativado, os nomes copiados ou arrastados usam a forma schema.tabela com as regras de aspas do identificador da conexão ativa.",
|
||||
sidebarTablePageSize: "Tamanho da página da barra lateral",
|
||||
sidebarTablePageSizeDescription: "Número máximo de tabelas/objetos carregados por página na árvore lateral. Aumente se tiver muitas tabelas para reduzir páginas.",
|
||||
sidebarTableSearchEnabled: "Ativar busca de tabelas por banco",
|
||||
|
||||
@@ -2868,6 +2868,7 @@ export default withEnglishFallback({
|
||||
openDamengJobAdmin: "打开达梦代理作业",
|
||||
newQuery: "新建查询",
|
||||
addToAi: "添加到 AI",
|
||||
addToAiMultiple: "添加到 AI({count} 张表)",
|
||||
instanceInfo: "实例信息",
|
||||
newInsert: "新建新增",
|
||||
newSql: "新建 SQL",
|
||||
@@ -2934,6 +2935,9 @@ export default withEnglishFallback({
|
||||
restoreSqliteDatabaseFailed: "还原 SQLite 数据库失败:{message}",
|
||||
importData: "导入数据",
|
||||
exportData: "导出数据",
|
||||
exportDataMultiple: "导出数据({count} 张表)",
|
||||
exportDataSelectDirectory: "选择导出文件夹",
|
||||
exportDataMultipleSuccess: "已导出 {count} 张表",
|
||||
exportStructure: "导出表结构",
|
||||
exportStructurePreviewTitle: "表结构 - {name}",
|
||||
exportStructurePreviewTitleMultiple: "表结构 - {count} 个对象",
|
||||
@@ -6498,6 +6502,10 @@ export default withEnglishFallback({
|
||||
sidebarHiddenTablePrefixes: "隐藏表名前缀",
|
||||
sidebarHiddenTablePrefixesDescription: "每行一个前缀,仅影响侧边栏表、视图和集合的显示名称,悬浮提示和实际操作仍使用完整名称。",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "例如:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "复制表名分隔符",
|
||||
sidebarCopyTableNameSeparatorDescription: "多选侧边栏表或字段后复制名称或拖拽到 SQL 编辑器时使用的分隔符。",
|
||||
sidebarCopyTableNameIncludeSchema: "复制表名包含 schema",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "开启后复制或拖拽表名时使用 schema.table 限定名,并按当前连接方言自动加引号。",
|
||||
sidebarTablePageSize: "侧边栏分页大小",
|
||||
sidebarTablePageSizeDescription: "侧边栏树每页最多加载的表/对象数量。表多时可以调大减少翻页次数。",
|
||||
sidebarTableSearchEnabled: "启用库下表搜索框",
|
||||
|
||||
@@ -2869,6 +2869,7 @@ export default withEnglishFallback({
|
||||
duplicateSelectedConnections: "複製選取的 {count} 個連線",
|
||||
newQuery: "建立查詢",
|
||||
addToAi: "加入 AI",
|
||||
addToAiMultiple: "加入 AI({count} 張表)",
|
||||
instanceInfo: "實例資訊",
|
||||
newSql: "新增 SQL",
|
||||
generateSql: "產生 SQL",
|
||||
@@ -2933,6 +2934,9 @@ export default withEnglishFallback({
|
||||
restoreSqliteDatabaseFailed: "還原 SQLite 資料庫失敗:{message}",
|
||||
importData: "匯入資料",
|
||||
exportData: "匯出資料",
|
||||
exportDataMultiple: "匯出資料({count} 張表)",
|
||||
exportDataSelectDirectory: "選擇匯出資料夾",
|
||||
exportDataMultipleSuccess: "已匯出 {count} 張表",
|
||||
exportStructure: "匯出資料表結構",
|
||||
exportStructurePreviewTitle: "資料表結構 - {name}",
|
||||
exportStructurePreviewTitleMultiple: "資料表結構 - {count} 個物件",
|
||||
@@ -5504,6 +5508,10 @@ export default withEnglishFallback({
|
||||
sidebarHiddenTablePrefixes: "隱藏資料表名稱字首",
|
||||
sidebarHiddenTablePrefixesDescription: "每行一個字首。只縮短側邊欄中的資料表、檢視和集合標籤;工具提示和實際操作仍使用完整名稱。",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "範例:\nODS_\nT8Y2_LONG_",
|
||||
sidebarCopyTableNameSeparator: "複製表名分隔符",
|
||||
sidebarCopyTableNameSeparatorDescription: "多選側邊欄表或欄位後複製名稱或拖曳到 SQL 編輯器時使用的分隔符。",
|
||||
sidebarCopyTableNameIncludeSchema: "複製表名包含 schema",
|
||||
sidebarCopyTableNameIncludeSchemaDescription: "開啟後複製或拖曳表名時使用 schema.table 限定名,並依目前連線方言自動加引號。",
|
||||
sidebarTablePageSize: "側邊欄分頁大小",
|
||||
sidebarTablePageSizeDescription: "側邊欄樹每頁最多載入的資料表/物件數量。資料表多時可調大減少翻頁次數。",
|
||||
sidebarTableSearchEnabled: "啟用庫下資料表搜尋框",
|
||||
|
||||
@@ -12,4 +12,14 @@ describe("add to AI database context", () => {
|
||||
expect(addToAiSource).toContain("else if (hasTreeNodeDatabaseContext(node))");
|
||||
expect(addToAiSource).not.toContain("else if (node.database)");
|
||||
});
|
||||
|
||||
it("adds every selected table as an AI mention", () => {
|
||||
const addToAiStart = appSource.indexOf("async function addToAi");
|
||||
const addToAiEnd = appSource.indexOf("function openAiPanel", addToAiStart);
|
||||
const addToAiSource = appSource.slice(addToAiStart, addToAiEnd);
|
||||
|
||||
expect(addToAiSource).toContain("Array.isArray(nodesInput) ? nodesInput : [nodesInput]");
|
||||
expect(addToAiSource).toContain('entry.type === "table"');
|
||||
expect(addToAiSource).toContain("for (const mention of tableMentions) handle.addTableMention(mention)");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createColumnReferencePayload, createTableReferencePayload, parseTableReferencePayload, tableReferenceInsertText } from "@/lib/editor/queryEditorTableDrop";
|
||||
import { createColumnReferencePayload, createMultiTableReferencePayload, createTableReferencePayload, parseTableReferencePayload, tableReferenceInsertText } from "@/lib/editor/queryEditorTableDrop";
|
||||
|
||||
const commaNewline = { columnNameSeparator: "comma-newline" as const };
|
||||
|
||||
describe("query editor table reference drop", () => {
|
||||
it("inserts a quoted database name for database references", () => {
|
||||
@@ -102,7 +104,7 @@ describe("query editor table reference drop", () => {
|
||||
columnNames: ["id", "order no", "order", "created_at"],
|
||||
databaseType: "mysql",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(mysql)).toBe("id,\n`order no`,\n`order`,\ncreated_at");
|
||||
expect(tableReferenceInsertText(mysql, undefined, commaNewline)).toBe("id,\n`order no`,\n`order`,\ncreated_at");
|
||||
|
||||
const postgres = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
@@ -110,7 +112,29 @@ describe("query editor table reference drop", () => {
|
||||
columnNames: ["id", "OrderNo", "select"],
|
||||
databaseType: "postgres",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(postgres)).toBe('id,\n"OrderNo",\n"select"');
|
||||
expect(tableReferenceInsertText(postgres, undefined, commaNewline)).toBe('id,\n"OrderNo",\n"select"');
|
||||
});
|
||||
|
||||
it("uses columnNameSeparator from the payload for multi-column sidebar drag", () => {
|
||||
const comma = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "name"],
|
||||
databaseType: "mysql",
|
||||
columnNameSeparator: "comma",
|
||||
})!;
|
||||
expect(comma.columnNameSeparator).toBe("comma");
|
||||
expect(parseTableReferencePayload(JSON.stringify(comma))).toEqual(comma);
|
||||
expect(tableReferenceInsertText(comma)).toBe("id,name");
|
||||
|
||||
const newline = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "name"],
|
||||
databaseType: "mysql",
|
||||
columnNameSeparator: "newline",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(newline)).toBe("id\nname");
|
||||
});
|
||||
|
||||
it("keeps smart quoting for dialects without a dedicated keyword table", () => {
|
||||
@@ -121,7 +145,7 @@ describe("query editor table reference drop", () => {
|
||||
columnNames: ["id", "order no"],
|
||||
databaseType: "oracle",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(oracle)).toBe('id,\n"order no"');
|
||||
expect(tableReferenceInsertText(oracle, undefined, commaNewline)).toBe('id,\n"order no"');
|
||||
});
|
||||
|
||||
it("bare-quotes sqlserver columns when safe and brackets reserved words", () => {
|
||||
@@ -131,7 +155,7 @@ describe("query editor table reference drop", () => {
|
||||
columnNames: ["id", "order", "order no"],
|
||||
databaseType: "sqlserver",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(payload)).toBe("id,\n[order],\n[order no]");
|
||||
expect(tableReferenceInsertText(payload, undefined, commaNewline)).toBe("id,\n[order],\n[order no]");
|
||||
});
|
||||
|
||||
it("falls back to the editor database type when the payload omits one", () => {
|
||||
@@ -140,6 +164,21 @@ describe("query editor table reference drop", () => {
|
||||
database: "app-db",
|
||||
columnNames: ["order no"],
|
||||
})!;
|
||||
expect(tableReferenceInsertText(payload, "mysql")).toBe("`order no`");
|
||||
expect(tableReferenceInsertText(payload, "mysql", commaNewline)).toBe("`order no`");
|
||||
});
|
||||
|
||||
it("inserts multiple table references with the configured separator", () => {
|
||||
const payload = createMultiTableReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
tableReferences: [
|
||||
{ schema: "public", tableName: "users" },
|
||||
{ schema: "public", tableName: "orders" },
|
||||
],
|
||||
databaseType: "postgres",
|
||||
})!;
|
||||
expect(parseTableReferencePayload(JSON.stringify(payload))).toEqual(payload);
|
||||
expect(tableReferenceInsertText(payload, undefined, { tableNameSeparator: "comma" })).toBe("users,orders");
|
||||
expect(tableReferenceInsertText(payload, undefined, { tableNameSeparator: "comma", includeTableSchema: true })).toBe('"public"."users","public"."orders"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { resolveSidebarDdlTargets } from "@/lib/sidebar/sidebarDdlTargets";
|
||||
|
||||
describe("sidebarDdlTargets", () => {
|
||||
it("resolves multi-select ddl targets in the same execution context", () => {
|
||||
const first: TreeNode = { id: "t1", label: "one", type: "table", connectionId: "c1", database: "db" };
|
||||
const second: TreeNode = { id: "t2", label: "two", type: "view", connectionId: "c1", database: "db" };
|
||||
const otherDb: TreeNode = { id: "t3", label: "three", type: "table", connectionId: "c1", database: "other" };
|
||||
const group: TreeNode = { id: "group", label: "Tables", type: "group-tables", children: [first, second, otherDb] };
|
||||
|
||||
expect(resolveSidebarDdlTargets(first, [group], [second.id, first.id])).toEqual([first, second]);
|
||||
expect(resolveSidebarDdlTargets(first, [group], [otherDb.id, first.id, second.id])).toEqual([first, second]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { allDatabasesExportSourceForNode, databaseExportSourceForNode, sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { allDatabasesExportSourceForNode, databaseExportSourceForNode, sidebarSameSchemaStructureTargets, sidebarStructureExportTargets, sidebarTableDataExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
|
||||
describe("sidebar export runtime", () => {
|
||||
it("prepares database and table export sources", () => {
|
||||
@@ -27,4 +27,36 @@ describe("sidebar export runtime", () => {
|
||||
expect(sidebarStructureExportTargets(first, [group], [third.id, first.id, second.id])).toEqual([first, second, third]);
|
||||
expect(sidebarStructureExportTargets(first, [group], [second.id])).toEqual([first]);
|
||||
});
|
||||
|
||||
it("limits multi-select diagram and database export prefills to the active schema context", () => {
|
||||
const publicUsers: TreeNode = { id: "t1", label: "users", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const publicOrders: TreeNode = { id: "t2", label: "orders", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const salesUsers: TreeNode = { id: "t3", label: "users", type: "table", connectionId: "c1", database: "db", schema: "sales" };
|
||||
const group: TreeNode = { id: "group", label: "Tables", type: "group-tables", children: [publicUsers, publicOrders, salesUsers] };
|
||||
|
||||
expect(sidebarSameSchemaStructureTargets(publicUsers, [group], [publicOrders.id, publicUsers.id, salesUsers.id])).toEqual([publicUsers, publicOrders]);
|
||||
expect(sidebarSameSchemaStructureTargets(publicUsers, [group], [salesUsers.id, publicUsers.id])).toEqual([publicUsers]);
|
||||
});
|
||||
|
||||
it("counts batch data export targets as tables only", () => {
|
||||
const table: TreeNode = { id: "t1", label: "users", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const view: TreeNode = { id: "v1", label: "active_users", type: "view", connectionId: "c1", database: "db", schema: "public" };
|
||||
const other: TreeNode = { id: "t2", label: "orders", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const group: TreeNode = { id: "group", label: "Objects", type: "group-tables", children: [table, view, other] };
|
||||
|
||||
expect(sidebarTableDataExportTargets(table, [group], [other.id, table.id, view.id])).toEqual([table, other]);
|
||||
expect(sidebarTableDataExportTargets(view, [group], [other.id, table.id, view.id])).toEqual([view]);
|
||||
expect(sidebarTableDataExportTargets(table, [group], [view.id])).toEqual([table]);
|
||||
});
|
||||
|
||||
it("scopes batch data export targets to the active execution context", () => {
|
||||
const localUsers: TreeNode = { id: "t1", label: "users", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const localOrders: TreeNode = { id: "t2", label: "orders", type: "table", connectionId: "c1", database: "db", schema: "public" };
|
||||
const otherConnectionOrders: TreeNode = { id: "t3", label: "orders", type: "table", connectionId: "c2", database: "db", schema: "public" };
|
||||
const otherDatabaseUsers: TreeNode = { id: "t4", label: "users", type: "table", connectionId: "c1", database: "other", schema: "public" };
|
||||
const group: TreeNode = { id: "group", label: "Tables", type: "group-tables", children: [localUsers, localOrders, otherConnectionOrders, otherDatabaseUsers] };
|
||||
|
||||
expect(sidebarTableDataExportTargets(localUsers, [group], [localUsers.id, localOrders.id, otherConnectionOrders.id, otherDatabaseUsers.id])).toEqual([localUsers, localOrders]);
|
||||
expect(sidebarTableDataExportTargets(localOrders, [group], [localUsers.id, localOrders.id, otherConnectionOrders.id, otherDatabaseUsers.id])).toEqual([localUsers, localOrders]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { columnNameForSidebarDrag, formatSidebarTableNamesForCopy, normalizeSidebarCopyTableNameSeparator, resolveSidebarColumnDragNames, resolveSidebarColumnDragTargets, resolveSidebarTableCopyTargets } from "@/lib/sidebar/sidebarTableNameCopy";
|
||||
|
||||
describe("sidebarTableNameCopy", () => {
|
||||
const table = (id: string, label: string, schema = "public"): TreeNode => ({
|
||||
id,
|
||||
label,
|
||||
type: "table",
|
||||
connectionId: "c1",
|
||||
database: "db",
|
||||
schema,
|
||||
});
|
||||
|
||||
const column = (id: string, label: string, name: string): TreeNode => ({
|
||||
id,
|
||||
label,
|
||||
type: "column",
|
||||
connectionId: "c1",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
meta: { name },
|
||||
});
|
||||
|
||||
it("resolves multi-table copy targets in visible order", () => {
|
||||
const first = table("t1", "one");
|
||||
const second = table("t2", "two");
|
||||
const third = table("t3", "three");
|
||||
expect(resolveSidebarTableCopyTargets(first, [third, first, second])).toEqual([third, first, second]);
|
||||
expect(resolveSidebarTableCopyTargets(first, [second])).toEqual([first]);
|
||||
});
|
||||
|
||||
it("falls back to active node when selection is mixed connection", () => {
|
||||
const first = table("t1", "one");
|
||||
const other = { ...table("t2", "two"), connectionId: "c2" };
|
||||
expect(resolveSidebarTableCopyTargets(first, [first, other])).toEqual([first]);
|
||||
});
|
||||
|
||||
it("formats bare and qualified table names", () => {
|
||||
const targets = [table("t1", "users"), table("t2", "orders", "sales")];
|
||||
expect(formatSidebarTableNamesForCopy(targets, { separator: "comma", includeSchema: false })).toBe("users,orders");
|
||||
expect(formatSidebarTableNamesForCopy(targets, { separator: "newline", includeSchema: true, databaseType: "postgres" })).toBe('"public"."users"\n"sales"."orders"');
|
||||
expect(formatSidebarTableNamesForCopy([table("t1", "users")], { separator: "comma", includeSchema: true, databaseType: "mysql" })).toBe("`users`");
|
||||
expect(formatSidebarTableNamesForCopy([table("t1", "users", "default")], { separator: "comma", includeSchema: true, databaseType: "hive", schema: "default" })).toBe("`default`.`users`");
|
||||
});
|
||||
|
||||
it("resolves multi-column drag targets for the same table", () => {
|
||||
const first = column("c1", "id (bigint)", "id");
|
||||
const second = column("c2", "name (text)", "name");
|
||||
expect(resolveSidebarColumnDragTargets(first, [second, first])).toEqual([second, first]);
|
||||
expect(resolveSidebarColumnDragNames(first, [second, first])).toEqual(["name", "id"]);
|
||||
});
|
||||
|
||||
it("falls back to active column when selection spans tables", () => {
|
||||
const first = column("c1", "id (bigint)", "id");
|
||||
const other = { ...column("c2", "name (text)", "name"), tableName: "orders" };
|
||||
expect(resolveSidebarColumnDragTargets(first, [first, other])).toEqual([first]);
|
||||
});
|
||||
|
||||
it("extracts column drag names from labels", () => {
|
||||
expect(columnNameForSidebarDrag(column("c1", "created_at (timestamp)", "created_at"))).toBe("created_at");
|
||||
});
|
||||
|
||||
it("normalizes separator settings", () => {
|
||||
expect(normalizeSidebarCopyTableNameSeparator("comma-newline")).toBe("comma-newline");
|
||||
expect(normalizeSidebarCopyTableNameSeparator("bogus")).toBe("comma");
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -80,6 +80,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"sidebarIndent",
|
||||
"sidebarFontSize",
|
||||
"sidebarHiddenTablePrefixes",
|
||||
"sidebarCopyTableNameSeparator",
|
||||
"sidebarCopyTableNameIncludeSchema",
|
||||
"redisKeyTemplates",
|
||||
"exportBatchSize",
|
||||
"exportRowLimitEnabled",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { sidebarDdlTargetsForExecutionContext } from "@/lib/sidebar/sidebarDdlTemplate";
|
||||
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
|
||||
export type SidebarDdlTarget = TreeNode & { connectionId: string; database: string };
|
||||
|
||||
export function resolveSidebarDdlTargets(activeNode: TreeNode, treeNodes: readonly TreeNode[], selectedNodeIds: readonly string[]): SidebarDdlTarget[] {
|
||||
const targets = sidebarStructureExportTargets(activeNode, treeNodes, selectedNodeIds);
|
||||
if (!targets.length || !activeNode.connectionId || !activeNode.database) return [];
|
||||
return sidebarDdlTargetsForExecutionContext(activeNode as SidebarDdlTarget, targets);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { sidebarDdlTargetsForExecutionContext } from "@/lib/sidebar/sidebarDdlTemplate";
|
||||
|
||||
export interface SidebarDatabaseExportSource {
|
||||
connectionId: string;
|
||||
@@ -25,15 +26,26 @@ export function allDatabasesExportSourceForNode(node: TreeNode): SidebarDatabase
|
||||
return { connectionId: node.connectionId, database: "", allDatabases: true };
|
||||
}
|
||||
|
||||
export function sidebarStructureExportTargets(activeNode: TreeNode, treeNodes: readonly TreeNode[], selectedNodeIds: readonly string[]): Array<TreeNode & { connectionId: string; database: string }> {
|
||||
const canExport = (node: TreeNode): node is TreeNode & { connectionId: string; database: string } => (node.type === "table" || node.type === "view" || node.type === "materialized_view") && !!node.connectionId && !!node.database;
|
||||
if (!canExport(activeNode)) return [];
|
||||
export type SidebarStructureExportTarget = TreeNode & { connectionId: string; database: string };
|
||||
|
||||
function canStructureExport(node: TreeNode): node is SidebarStructureExportTarget {
|
||||
return (node.type === "table" || node.type === "view" || node.type === "materialized_view") && !!node.connectionId && !!node.database;
|
||||
}
|
||||
|
||||
/** connection + database + catalog + schema identity for diagram and database-export prefills. */
|
||||
export function sidebarStructureTargetSchemaContext(node: TreeNode): string {
|
||||
if (!node.connectionId || !node.database) return "";
|
||||
return `${node.connectionId}\0${node.database}\0${node.catalog ?? ""}\0${node.schema ?? ""}`;
|
||||
}
|
||||
|
||||
export function sidebarStructureExportTargets(activeNode: TreeNode, treeNodes: readonly TreeNode[], selectedNodeIds: readonly string[]): SidebarStructureExportTarget[] {
|
||||
if (!canStructureExport(activeNode)) return [];
|
||||
|
||||
const selected = new Set(selectedNodeIds);
|
||||
const targets: Array<TreeNode & { connectionId: string; database: string }> = [];
|
||||
const targets: SidebarStructureExportTarget[] = [];
|
||||
const visit = (nodes: readonly TreeNode[]) => {
|
||||
for (const node of nodes) {
|
||||
if (selected.has(node.id) && canExport(node)) targets.push(node);
|
||||
if (selected.has(node.id) && canStructureExport(node)) targets.push(node);
|
||||
if (node.children) visit(node.children);
|
||||
if (node.hiddenChildren) visit(node.hiddenChildren);
|
||||
}
|
||||
@@ -41,3 +53,23 @@ export function sidebarStructureExportTargets(activeNode: TreeNode, treeNodes: r
|
||||
visit(treeNodes);
|
||||
return targets.length > 1 && targets.some((node) => node.id === activeNode.id) ? targets : [activeNode];
|
||||
}
|
||||
|
||||
/** Keeps multi-select only when every target shares the active node's schema context. */
|
||||
export function sidebarSameSchemaStructureTargets(activeNode: TreeNode, treeNodes: readonly TreeNode[], selectedNodeIds: readonly string[]): SidebarStructureExportTarget[] {
|
||||
if (!canStructureExport(activeNode)) return [];
|
||||
const targets = sidebarStructureExportTargets(activeNode, treeNodes, selectedNodeIds);
|
||||
if (targets.length <= 1) return targets;
|
||||
const activeContext = sidebarStructureTargetSchemaContext(activeNode);
|
||||
const sameContext = targets.filter((target) => sidebarStructureTargetSchemaContext(target) === activeContext);
|
||||
return sameContext.length > 1 && sameContext.some((target) => target.id === activeNode.id) ? sameContext : [activeNode];
|
||||
}
|
||||
|
||||
/** Batch data export includes tables only and stays inside the active execution context; single-target fallback still allows one view/materialized view. */
|
||||
export function sidebarTableDataExportTargets(activeNode: TreeNode, treeNodes: readonly TreeNode[], selectedNodeIds: readonly string[]): SidebarStructureExportTarget[] {
|
||||
const targets = canStructureExport(activeNode) ? sidebarDdlTargetsForExecutionContext(activeNode, sidebarStructureExportTargets(activeNode, treeNodes, selectedNodeIds)) : [];
|
||||
const tables = targets.filter((node) => node.type === "table");
|
||||
if (tables.length > 1 && tables.some((node) => node.id === activeNode.id)) return tables;
|
||||
if (activeNode.type === "table" && canStructureExport(activeNode)) return [activeNode];
|
||||
if (canStructureExport(activeNode) && targets.some((node) => node.id === activeNode.id)) return [activeNode];
|
||||
return tables;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { ColumnInfo, DatabaseType, TreeNode } from "@/types/database";
|
||||
import { COLUMN_NAME_COPY_SEPARATOR_VALUES, type ColumnNameCopySeparator, isColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
|
||||
import { copyNameForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { qualifiedTableName } from "@/lib/table/tableSelectSql";
|
||||
|
||||
export type SidebarTableCopyTarget = TreeNode & { connectionId: string; database: string };
|
||||
|
||||
const TABLE_COPY_NODE_TYPES = new Set<TreeNode["type"]>(["table", "view", "materialized_view"]);
|
||||
|
||||
function isTableCopyTarget(node: TreeNode): node is SidebarTableCopyTarget {
|
||||
return TABLE_COPY_NODE_TYPES.has(node.type) && !!node.connectionId && !!node.database;
|
||||
}
|
||||
|
||||
export function columnNameForSidebarDrag(node: TreeNode): string {
|
||||
if (node.type !== "column") return copyNameForTreeNode(node);
|
||||
const column = node.meta as Partial<ColumnInfo> | undefined;
|
||||
if (typeof column?.name === "string" && column.name) return column.name;
|
||||
return node.label.replace(/\s+\([^()]*\)$/, "");
|
||||
}
|
||||
|
||||
function sameColumnTableContext(first: TreeNode, node: TreeNode): boolean {
|
||||
return node.type === "column" && !!node.connectionId && node.database != null && node.connectionId === first.connectionId && node.database === first.database && (node.schema ?? "") === (first.schema ?? "") && !!node.tableName && node.tableName === first.tableName;
|
||||
}
|
||||
|
||||
export function resolveSidebarTableCopyTargets(activeNode: TreeNode, selectedNodes: readonly TreeNode[]): SidebarTableCopyTarget[] {
|
||||
if (!isTableCopyTarget(activeNode)) return [];
|
||||
if (selectedNodes.length <= 1 || !selectedNodes.some((node) => node.id === activeNode.id)) return [activeNode];
|
||||
const targets = selectedNodes.filter(isTableCopyTarget);
|
||||
if (targets.length <= 1) return [activeNode];
|
||||
const first = targets[0]!;
|
||||
if (!targets.every((node) => node.connectionId === first.connectionId && node.database === first.database && (node.catalog ?? "") === (first.catalog ?? ""))) {
|
||||
return [activeNode];
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
export function resolveSidebarColumnDragTargets(activeNode: TreeNode, selectedNodes: readonly TreeNode[]): TreeNode[] {
|
||||
if (activeNode.type !== "column" || !activeNode.tableName) return [activeNode];
|
||||
if (selectedNodes.length <= 1 || !selectedNodes.some((node) => node.id === activeNode.id)) return [activeNode];
|
||||
const targets = selectedNodes.filter((node) => node.type === "column" && sameColumnTableContext(activeNode, node));
|
||||
return targets.length > 1 ? targets : [activeNode];
|
||||
}
|
||||
|
||||
export function resolveSidebarColumnDragNames(activeNode: TreeNode, selectedNodes: readonly TreeNode[]): string[] {
|
||||
return resolveSidebarColumnDragTargets(activeNode, selectedNodes)
|
||||
.map(columnNameForSidebarDrag)
|
||||
.filter((name) => name.length > 0);
|
||||
}
|
||||
|
||||
export interface FormatSidebarTableNamesOptions {
|
||||
separator: ColumnNameCopySeparator;
|
||||
includeSchema: boolean;
|
||||
databaseType?: DatabaseType;
|
||||
driverProfile?: string;
|
||||
identifierQuote?: string;
|
||||
}
|
||||
|
||||
export function formatSidebarTableNamesForCopy(targets: readonly SidebarTableCopyTarget[], options: FormatSidebarTableNamesOptions): string {
|
||||
const separator = COLUMN_NAME_COPY_SEPARATOR_VALUES[options.separator] ?? ",";
|
||||
const parts = targets.map((target) => {
|
||||
if (!options.includeSchema) return copyNameForTreeNode(target);
|
||||
return qualifiedTableName({
|
||||
databaseType: options.databaseType,
|
||||
driverProfile: options.driverProfile,
|
||||
identifierQuote: options.identifierQuote,
|
||||
schema: target.schema,
|
||||
tableName: target.label,
|
||||
database: target.database,
|
||||
catalog: target.catalog,
|
||||
});
|
||||
});
|
||||
return parts.join(separator);
|
||||
}
|
||||
|
||||
export function normalizeSidebarCopyTableNameSeparator(value: unknown): ColumnNameCopySeparator {
|
||||
return isColumnNameCopySeparator(value) ? value : "comma";
|
||||
}
|
||||
|
||||
export function sidebarTableCopyNodes(activeNode: TreeNode, selectedNodes: readonly TreeNode[]): TreeNode[] {
|
||||
return selectedNodes.length > 1 && selectedNodes.some((node) => node.id === activeNode.id) ? [...selectedNodes] : [activeNode];
|
||||
}
|
||||
|
||||
export function formatSidebarTableCopyText(activeNode: TreeNode, selectedNodes: readonly TreeNode[], options: FormatSidebarTableNamesOptions): string {
|
||||
const tableTargets = resolveSidebarTableCopyTargets(activeNode, selectedNodes);
|
||||
const separator = COLUMN_NAME_COPY_SEPARATOR_VALUES[options.separator] ?? ",";
|
||||
if (tableTargets.length > 0 && (tableTargets.length > 1 || options.includeSchema)) {
|
||||
return formatSidebarTableNamesForCopy(tableTargets, options);
|
||||
}
|
||||
return sidebarTableCopyNodes(activeNode, selectedNodes).map(copyNameForTreeNode).join(separator);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export interface SidebarTreeRuntimeHost {
|
||||
handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
|
||||
openPrimaryVisibleFilter(node: TreeNode): void;
|
||||
openDataInNewTab(node: TreeNode): void;
|
||||
openDdlForSelection(node: TreeNode, selectedNodeIds: readonly string[]): Promise<boolean>;
|
||||
requestPaste(node: TreeNode): boolean;
|
||||
toggleNode(node: TreeNode): void;
|
||||
}
|
||||
|
||||
@@ -518,6 +518,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
database: string;
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
tableNames?: string[];
|
||||
} | null>(null);
|
||||
const docsSource = ref<{
|
||||
connectionId: string;
|
||||
|
||||
@@ -15,8 +15,10 @@ import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/co
|
||||
import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
|
||||
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
|
||||
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { normalizeSidebarCopyTableNameSeparator } from "@/lib/sidebar/sidebarTableNameCopy";
|
||||
import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
|
||||
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
|
||||
import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
|
||||
@@ -670,6 +672,8 @@ export interface EditorSettings {
|
||||
formatSqlOnSqlFileSave: boolean;
|
||||
updateNotificationsEnabled: boolean;
|
||||
sidebarHiddenTablePrefixes: string[];
|
||||
sidebarCopyTableNameSeparator: ColumnNameCopySeparator;
|
||||
sidebarCopyTableNameIncludeSchema: boolean;
|
||||
sidebarObjectInfoMode: SidebarObjectInfoMode;
|
||||
sidebarShowConnectionNotes: boolean;
|
||||
sidebarAllowHorizontalScroll: boolean;
|
||||
@@ -888,6 +892,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
formatSqlOnSqlFileSave: false,
|
||||
updateNotificationsEnabled: true,
|
||||
sidebarHiddenTablePrefixes: [],
|
||||
sidebarCopyTableNameSeparator: "comma",
|
||||
sidebarCopyTableNameIncludeSchema: false,
|
||||
sidebarObjectInfoMode: "comment-inline",
|
||||
sidebarShowConnectionNotes: false,
|
||||
sidebarAllowHorizontalScroll: false,
|
||||
@@ -1322,6 +1328,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
formatSqlOnSqlFileSave: settings.formatSqlOnSqlFileSave === true,
|
||||
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
|
||||
sidebarCopyTableNameSeparator: normalizeSidebarCopyTableNameSeparator(settings.sidebarCopyTableNameSeparator),
|
||||
sidebarCopyTableNameIncludeSchema: settings.sidebarCopyTableNameIncludeSchema === true,
|
||||
sidebarObjectInfoMode: normalizeSidebarObjectInfoMode(
|
||||
settings.sidebarObjectInfoMode,
|
||||
(
|
||||
@@ -1925,6 +1933,8 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.formatSqlOnSqlFileSave !== undefined) editorSettings.value.formatSqlOnSqlFileSave = partial.formatSqlOnSqlFileSave === true;
|
||||
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
|
||||
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);
|
||||
if (partial.sidebarCopyTableNameSeparator !== undefined) editorSettings.value.sidebarCopyTableNameSeparator = normalizeSidebarCopyTableNameSeparator(partial.sidebarCopyTableNameSeparator);
|
||||
if (partial.sidebarCopyTableNameIncludeSchema !== undefined) editorSettings.value.sidebarCopyTableNameIncludeSchema = partial.sidebarCopyTableNameIncludeSchema === true;
|
||||
if (partial.sidebarObjectInfoMode !== undefined) editorSettings.value.sidebarObjectInfoMode = normalizeSidebarObjectInfoMode(partial.sidebarObjectInfoMode);
|
||||
if (partial.sidebarShowConnectionNotes !== undefined) editorSettings.value.sidebarShowConnectionNotes = partial.sidebarShowConnectionNotes === true;
|
||||
if (partial.sidebarAllowHorizontalScroll !== undefined) editorSettings.value.sidebarAllowHorizontalScroll = partial.sidebarAllowHorizontalScroll;
|
||||
|
||||
Reference in new issue
Block a user