feat(sidebar): batch actions for multi selected nodes

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

No files matched your search

+7 -3
View File
@@ -944,8 +944,10 @@ function sendSelectionToAi(sql: string) {
let addToAiRequestId = 0; let addToAiRequestId = 0;
async function addToAi(node: TreeNode) { async function addToAi(nodesInput: TreeNode | TreeNode[]) {
if ((node.type !== "connection" && node.type !== "database" && node.type !== "table") || !node.connectionId) return; 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); const connection = connectionStore.getConfig(node.connectionId);
if (!connection) return; if (!connection) return;
const requestId = ++addToAiRequestId; 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); 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"); openRightSidebarPanel("ai");
invokeWhenAiReady((handle) => { invokeWhenAiReady((handle) => {
if (contextChanged) handle.clearContextReferences(); 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) { } catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000); toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
@@ -272,6 +272,7 @@ const props = defineProps<{
prefillDatabase?: string; prefillDatabase?: string;
prefillSchema?: string; prefillSchema?: string;
focusTableName?: string; focusTableName?: string;
focusTableNames?: string[];
}>(); }>();
const emit = defineEmits<{ 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 allRelationships = computed(() => buildDiagramRelationships(tables.value, customRelationships.value));
const relatedTableNames = computed(() => { 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 focus = props.focusTableName;
const names = new Set<string>(); const names = new Set<string>();
if (!focus) return names; if (!focus) return names;
@@ -411,6 +414,9 @@ const visibleTables = computed(() => {
tables.value.filter((table) => !table.pendingDrop), tables.value.filter((table) => !table.pendingDrop),
tableSearch.value, 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()) { if (props.focusTableName && !showAllTables.value && !tableSearch.value.trim()) {
return filtered.filter((table) => relatedTableNames.value.has(table.name)); return filtered.filter((table) => relatedTableNames.value.has(table.name));
} }
@@ -149,6 +149,7 @@ import {
import { eventToModifierOnlyShortcut, eventToShortcut } from "@/lib/editor/keyboardShortcuts"; import { eventToModifierOnlyShortcut, eventToShortcut } from "@/lib/editor/keyboardShortcuts";
import { SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry"; import { SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay"; 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 { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates"; import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame"; import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame";
@@ -524,6 +525,8 @@ const editShowTableDdlHoverPreview = ref(settingsStore.editorSettings.showTableD
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget); const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled); const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n")); 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 editRedisKeyTemplates = ref(normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n"));
const editSidebarObjectInfoMode = ref<SidebarObjectInfoMode>(settingsStore.editorSettings.sidebarObjectInfoMode); const editSidebarObjectInfoMode = ref<SidebarObjectInfoMode>(settingsStore.editorSettings.sidebarObjectInfoMode);
const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll); const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll);
@@ -672,6 +675,8 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
sidebarIndent: editSidebarIndent.value, sidebarIndent: editSidebarIndent.value,
sidebarFontSize: editSidebarFontSize.value, sidebarFontSize: editSidebarFontSize.value,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value), sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
sidebarCopyTableNameSeparator: editSidebarCopyTableNameSeparator.value,
sidebarCopyTableNameIncludeSchema: editSidebarCopyTableNameIncludeSchema.value,
redisKeyTemplates: normalizeRedisKeyTemplates(editRedisKeyTemplates.value), redisKeyTemplates: normalizeRedisKeyTemplates(editRedisKeyTemplates.value),
exportBatchSize: editExportBatchSize.value, exportBatchSize: editExportBatchSize.value,
globalDateTimeDisplayFormat: editGlobalDateTimeDisplayFormat.value, globalDateTimeDisplayFormat: editGlobalDateTimeDisplayFormat.value,
@@ -1066,6 +1071,8 @@ function syncEditorSettingsDraftFromStore() {
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget; editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled; editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"); 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"); editRedisKeyTemplates.value = normalizeRedisKeyTemplates(settingsStore.editorSettings.redisKeyTemplates).join("\n");
editSidebarObjectInfoMode.value = settingsStore.editorSettings.sidebarObjectInfoMode; editSidebarObjectInfoMode.value = settingsStore.editorSettings.sidebarObjectInfoMode;
editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll; editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll;
@@ -1332,6 +1339,8 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editSidebarIndent.value = DEFAULT_EDITOR_SETTINGS.sidebarIndent; editSidebarIndent.value = DEFAULT_EDITOR_SETTINGS.sidebarIndent;
editSidebarFontSize.value = DEFAULT_EDITOR_SETTINGS.sidebarFontSize; editSidebarFontSize.value = DEFAULT_EDITOR_SETTINGS.sidebarFontSize;
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n"); 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 }; editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems };
} else if (tab === "data") { } else if (tab === "data") {
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader; editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
@@ -1466,6 +1475,8 @@ function resetAllDefaults() {
editSidebarIndent.value = DEFAULT_EDITOR_SETTINGS.sidebarIndent; editSidebarIndent.value = DEFAULT_EDITOR_SETTINGS.sidebarIndent;
editSidebarFontSize.value = DEFAULT_EDITOR_SETTINGS.sidebarFontSize; editSidebarFontSize.value = DEFAULT_EDITOR_SETTINGS.sidebarFontSize;
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n"); 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"); editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize; editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat; editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
@@ -5733,6 +5744,40 @@ onUnmounted(() => {
{{ t("settings.sidebarHiddenTablePrefixesDescription") }} {{ t("settings.sidebarHiddenTablePrefixesDescription") }}
</p> </p>
</div> </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="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1"> <div class="space-y-1">
<Label for="sidebar-table-page-size">{{ t("settings.sidebarTablePageSize") }}</Label> <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 { function insertTableReferencePayload(currentView: EditorViewType, payload: QueryEditorTableReferencePayload, coords?: { clientX: number; clientY: number }): boolean {
if (props.readOnly) return false; 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 dropPos = coords ? currentView.posAtCoords({ x: coords.clientX, y: coords.clientY }) : null;
const selection = currentView.state.selection.main; const selection = currentView.state.selection.main;
const from = dropPos ?? selection.from; const from = dropPos ?? selection.from;
@@ -272,6 +272,7 @@ watch(
:prefill-database="dialogs.diagramPrefillDatabase.value" :prefill-database="dialogs.diagramPrefillDatabase.value"
:prefill-schema="dialogs.diagramPrefillSchema.value" :prefill-schema="dialogs.diagramPrefillSchema.value"
:focus-table-name="dialogs.diagramFocusTableName.value" :focus-table-name="dialogs.diagramFocusTableName.value"
:focus-table-names="dialogs.diagramFocusTableNames.value"
@open-target="emit('openDiagramTarget', $event)" @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" /> <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]; startResize: [event: MouseEvent];
collapse: []; collapse: [];
"open-settings": [initialTab: string]; "open-settings": [initialTab: string];
"add-to-ai": [node: TreeNode]; "add-to-ai": [nodes: TreeNode | TreeNode[]];
}>(); }>();
type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip"; type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip";
@@ -293,7 +293,7 @@ defineExpose({ focusSearch, locateTabInSidebar });
</template> </template>
</div> </div>
<div class="flex-1 min-h-0"> <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> </div>
<div class="panel-resize-handle panel-resize-handle--right" @mousedown="emit('startResize', $event)" /> <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 { buildTableTreeNodes } from "@/lib/table/tableTree";
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts"; import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts";
import { sidebarNodeSupportsDdlView } from "@/lib/sidebar/sidebarTreeDdlShortcut"; 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 { supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
import { copyToClipboard } from "@/lib/common/clipboard"; import { copyToClipboard } from "@/lib/common/clipboard";
import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection"; import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
import { formatSidebarTableCopyText } from "@/lib/sidebar/sidebarTableNameCopy";
import { pruneTreeSelectionToVisibleNodeIds } from "@/lib/sidebar/sidebarTreeSelection"; import { pruneTreeSelectionToVisibleNodeIds } from "@/lib/sidebar/sidebarTreeSelection";
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch"; import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
import { isInternalDorisCatalog, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport"; import { isInternalDorisCatalog, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
@@ -116,7 +117,7 @@ const sidebarContextMenuRef = ref<{ close: () => void } | null>(null);
const sidebarContextMenuItems = ref<ContextMenuItem[]>([]); const sidebarContextMenuItems = ref<ContextMenuItem[]>([]);
const emit = defineEmits<{ const emit = defineEmits<{
"open-settings": [initialTab: string]; "open-settings": [initialTab: string];
"add-to-ai": [node: TreeNode]; "add-to-ai": [nodes: TreeNode | TreeNode[]];
}>(); }>();
const sidebarContextMenuTarget = ref<SidebarActionTarget | null>(null); const sidebarContextMenuTarget = ref<SidebarActionTarget | null>(null);
@@ -1959,7 +1960,7 @@ function openSidebarDdlForSelection(): boolean {
const selectedNodeId = store.selectedTreeNodeId; const selectedNodeId = store.selectedTreeNodeId;
const node = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null; const node = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null;
if (!node || !sidebarNodeSupportsDdlView(node)) return false; if (!node || !sidebarNodeSupportsDdlView(node)) return false;
openSidebarDdl(node); void sidebarTreeRuntimeHostRef.value?.openDdlForSelection?.(node, store.selectedTreeNodeIds);
return true; return true;
} }
@@ -2335,7 +2336,17 @@ function copySelectedSidebarNames(): boolean {
})), })),
} }
: null; : 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)) .then(() => toast(t("connection.copied"), 2000))
.catch((e: any) => toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000)); .catch((e: any) => toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000));
return true; return true;
@@ -2420,7 +2431,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
@open-visible-schemas="openSidebarVisibleSchemas" @open-visible-schemas="openSidebarVisibleSchemas"
@open-visible-nacos-namespaces="openSidebarVisibleNacosNamespaces" @open-visible-nacos-namespaces="openSidebarVisibleNacosNamespaces"
@open-table-name-filters="openSidebarTableNameFilters" @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-connection-rename="startRenamingConnectionNode"
@request-group-rename="startRenamingCreatedGroup" @request-group-rename="startRenamingCreatedGroup"
@request-saved-sql-rename="startRenamingSavedSqlNode" @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 { elasticsearchClearIndexPreview, isElasticsearchClearConfirmed, isElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions";
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates"; import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates"; import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
import { joinExportedDdls } from "@/lib/export/ddlExport";
import { qualifiedTableName } from "@/lib/table/tableSelectSql"; import { qualifiedTableName } from "@/lib/table/tableSelectSql";
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint"; import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
import { import {
@@ -177,8 +178,10 @@ import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty"; import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty";
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate"; import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
import { runBatchTableDrop } from "@/lib/table/batchTableDrop"; import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
import { buildSidebarDdlTemplateSql, sidebarDdlTargetsForExecutionContext } from "@/lib/sidebar/sidebarDdlTemplate"; import { buildSidebarDdlTemplateSql } from "@/lib/sidebar/sidebarDdlTemplate";
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime"; 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 { supportsScheduledDatabaseBackup } from "@/lib/backup/scheduledDatabaseBackup";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { copyToClipboard } from "@/lib/common/clipboard"; import { copyToClipboard } from "@/lib/common/clipboard";
@@ -389,6 +392,7 @@ const { openAllDatabasesExport, openDataCompare, openDatabaseExport, openDatabas
queryStore, queryStore,
settingsStore, settingsStore,
tableChildObjectName: tableChildDropObjectName, tableChildObjectName: tableChildDropObjectName,
acceptedSelectionIds: () => acceptedSelectionIds,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@@ -409,7 +413,7 @@ const emit = defineEmits<{
"open-visible-schemas": [node: TreeNode]; "open-visible-schemas": [node: TreeNode];
"open-visible-nacos-namespaces": [node: TreeNode]; "open-visible-nacos-namespaces": [node: TreeNode];
"open-table-name-filters": [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-danger-dialog": [request: SidebarDangerDialogRequest];
"open-dialog-controller": [controller: Record<string, any> | null]; "open-dialog-controller": [controller: Record<string, any> | null];
"open-install-extension": [node: TreeNode]; "open-install-extension": [node: TreeNode];
@@ -637,6 +641,21 @@ function databaseTypeForNode(node: TreeNode): DatabaseType | undefined {
return node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : 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 { function tableStructureDatabaseTypeForNode(node: TreeNode): DatabaseType | undefined {
return node.connectionId ? tableStructureDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : 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"); queryStore.createTab(node.connectionId, "0", `${dbName} - ${t("contextMenu.instanceInfo")}`, "redis-dashboard");
} }
async function loadTemplateContext(allowView = false) { async function loadTemplateContext(allowView = false, node: TreeNode = activeNode.value) {
const node = activeNode.value;
if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return null; if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return null;
const isTableNode = node.type === "table"; const isTableNode = node.type === "table";
const isReadableObject = isTableNode || (allowView && (node.type === "view" || node.type === "materialized_view")); 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 }; 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) { 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 }); const tabId = queryStore.createTab(connectionId, database, title, "query", schema, undefined, catalog, { forceWordWrap: true });
queryStore.updateSql(tabId, sql); queryStore.updateSql(tabId, sql);
@@ -1798,6 +1830,27 @@ function openSqlTemplateTab(connectionId: string, database: string, schema: stri
async function newSelectTemplate() { async function newSelectTemplate() {
try { 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); const context = await loadTemplateContext(true);
if (!context) return; if (!context) return;
const sql = buildTableSelectTemplate({ const sql = buildTableSelectTemplate({
@@ -1819,6 +1872,28 @@ async function newSelectTemplate() {
async function newInsertTemplate() { async function newInsertTemplate() {
try { 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); const context = await loadTemplateContext(false);
if (!context) return; if (!context) return;
const sql = buildTableInsertTemplate({ const sql = buildTableInsertTemplate({
@@ -1841,6 +1916,27 @@ async function newInsertTemplate() {
async function newUpdateTemplate() { async function newUpdateTemplate() {
try { 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); const context = await loadTemplateContext(false);
if (!context) return; if (!context) return;
const sql = buildTableUpdateTemplate({ const sql = buildTableUpdateTemplate({
@@ -1862,6 +1958,27 @@ async function newUpdateTemplate() {
async function newDeleteTemplate() { async function newDeleteTemplate() {
try { 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); const context = await loadTemplateContext(false);
if (!context) return; if (!context) return;
const sql = buildTableDeleteTemplate({ 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() { async function generateDdlTemplate() {
if (currentDatabaseType() === "victoriametrics") return; if (currentDatabaseType() === "victoriametrics") return;
const targets = selectedDdlTargets(); const targets = selectedDdlTargets();
if (!targets.length) return; if (!targets.length) return;
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
try { try {
const sql = await buildSidebarDdlTemplateSql( await openSidebarMultiTableDdlTab(targets);
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);
} catch (e: any) { } catch (e: any) {
toast(e?.message || String(e), 5000); toast(e?.message || String(e), 5000);
} }
} }
function selectedDdlTargets() { function selectedDdlTargets() {
const targets = sidebarStructureExportTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds); return resolveSidebarDdlTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds);
return sidebarDdlTargetsForExecutionContext(activeNode.value as TreeNode & { connectionId: string; database: string }, targets);
} }
async function openDdl() { async function openDdl() {
@@ -1923,12 +2043,35 @@ async function openDdl() {
const targets = selectedDdlTargets(); const targets = selectedDdlTargets();
if (!targets.length) return; if (!targets.length) return;
if (targets.length > 1) { if (targets.length > 1) {
await exportStructure(); try {
await openSidebarMultiTableDdlTab(targets);
} catch (e: any) {
toast(e?.message || String(e), 5000);
}
return; return;
} }
emit("open-ddl", targets[0]!); 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) { function openElasticsearchIndexMetadata(kind: ElasticsearchIndexMetadataKind) {
emit("open-elasticsearch-index-metadata", createSidebarActionTarget(activeNode.value), kind); emit("open-elasticsearch-index-metadata", createSidebarActionTarget(activeNode.value), kind);
} }
@@ -1944,9 +2087,8 @@ async function refresh() {
} }
async function copyName() { async function copyName() {
const node = activeNode.value;
try { try {
await copyToClipboard(copyNameForTreeNode(node)); await copyToClipboard(formatSelectedTableNamesForClipboard());
toast(t("connection.copied"), 2000); toast(t("connection.copied"), 2000);
} catch (e: any) { } catch (e: any) {
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
@@ -2038,7 +2180,7 @@ async function copySelectedNames() {
} }
updateTreeClipboardForNodes(nodes); updateTreeClipboardForNodes(nodes);
try { try {
await copyToClipboard(nodes.map(copyNameForTreeNode).join("\n")); await copyToClipboard(formatSelectedTableNamesForClipboard(selectedNodes));
toast(t("connection.copied"), 2000); toast(t("connection.copied"), 2000);
} catch (e: any) { } catch (e: any) {
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
@@ -4844,7 +4986,26 @@ const shortcutRefresh = "F5";
const shortcutDelete = "Delete"; 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 { function exportDataSubmenu(includeSqlInsert = true): ContextMenuItem {
const count = selectedDataExportTargetCount();
const children: ContextMenuItem[] = [ const children: ContextMenuItem[] = [
{ label: "CSV", action: () => exportData("csv") }, { label: "CSV", action: () => exportData("csv") },
{ label: "JSON", action: () => exportData("json") }, { label: "JSON", action: () => exportData("json") },
@@ -4852,7 +5013,7 @@ function exportDataSubmenu(includeSqlInsert = true): ContextMenuItem {
if (includeSqlInsert) children.push({ label: "SQL INSERT", action: () => exportData("sql") }); if (includeSqlInsert) children.push({ label: "SQL INSERT", action: () => exportData("sql") });
children.push({ label: "XLSX", action: () => exportDataXlsx() }); children.push({ label: "XLSX", action: () => exportDataXlsx() });
return { return {
label: t("contextMenu.exportData"), label: count > 1 ? t("contextMenu.exportDataMultiple", { count }) : t("contextMenu.exportData"),
icon: Upload, icon: Upload,
children, children,
}; };
@@ -4974,7 +5135,7 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
if (supportsQueryActions) { if (supportsQueryActions) {
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare }); items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (supportsAiContext) { 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; const connectionWorkspace = node.connectionId ? driverProfileDatabaseWorkspace(connectionStore.getConfig(node.connectionId)?.driver_profile) : undefined;
@@ -5190,7 +5351,7 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
if (supportsConnectionQueryActions(currentDatabaseType())) { if (supportsConnectionQueryActions(currentDatabaseType())) {
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare }); items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (node.type === "database" && supportsAiAssistantContext(currentDatabaseType())) { 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(); const sqlHistoryMenu = savedSqlHistorySubmenu();
if (sqlHistoryMenu) items.push(sqlHistoryMenu); 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.openInNewDataTab"), action: openDataInNewTabImmediately, icon: CopyPlus });
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare }); items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (supportsAiAssistantContext(currentDatabaseType())) { 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({ label: "", separator: true });
items.push(exportDataSubmenu(false)); items.push(exportDataSubmenu(false));
@@ -5535,7 +5696,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
items.push(copyNameMenuItem()); items.push(copyNameMenuItem());
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare }); items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (node.type === "table" && supportsAiAssistantContext(currentDatabaseType())) { 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: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: openDataImmediately, icon: TableProperties }); items.push({ label: t("contextMenu.viewData"), action: openDataImmediately, icon: TableProperties });
@@ -6088,6 +6249,7 @@ defineExpose({
handleRowKeydown, handleRowKeydown,
openPrimaryVisibleFilter, openPrimaryVisibleFilter,
openDataInNewTab, openDataInNewTab,
openDdlForSelection,
requestPaste, requestPaste,
toggleNode, toggleNode,
}); });
@@ -53,7 +53,17 @@ import { Switch } from "@/components/ui/switch";
import LightTooltip from "@/components/ui/LightTooltip.vue"; import LightTooltip from "@/components/ui/LightTooltip.vue";
import type { ColumnInfo, ConnectionConfig, CustomTypeTreeMemberMeta, DatabaseType, TreeNode, TriggerInfo } from "@/types/database"; 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 { 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 { AI_ASSISTANT_TABLE_DROP_ROOT_SELECTOR } from "@/lib/ai/aiTableReferenceDrop";
import { beginTableReferenceDragFeedback, isOverSqlEditorTarget, type TableReferenceDragFeedback } from "@/lib/editor/tableReferenceDragFeedback"; import { beginTableReferenceDragFeedback, isOverSqlEditorTarget, type TableReferenceDragFeedback } from "@/lib/editor/tableReferenceDragFeedback";
import { formatSidebarObjectStorage } from "@/lib/sidebar/sidebarDatabaseStorage"; import { formatSidebarObjectStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
@@ -64,7 +74,8 @@ import { encodeSpannerResourcePath } from "@/lib/connection/spannerResourcePath"
import { hexToRgba } from "@/lib/common/color"; import { hexToRgba } from "@/lib/common/color";
import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay"; import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebar/sidebarLabelTooltip"; 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 { applyConnectionMultiSelection, applyTreeNodeSelection, connectionMultiSelectionAfterToggle } from "@/lib/sidebar/sidebarConnectionMultiSelect";
import { connectionBearingGroupIdsUnder, connectionIdsUnderGroup } from "@/lib/sidebar/sidebarLayout"; import { connectionBearingGroupIdsUnder, connectionIdsUnderGroup } from "@/lib/sidebar/sidebarLayout";
import { isSidebarDatabaseOpenForVisual } from "@/lib/sidebar/sidebarDatabaseOpenState"; import { isSidebarDatabaseOpenForVisual } from "@/lib/sidebar/sidebarDatabaseOpenState";
@@ -1172,13 +1183,31 @@ let referenceDragFeedback: TableReferenceDragFeedback | null = null;
let suppressNextTableReferenceClick = false; let suppressNextTableReferenceClick = false;
function selectedTreeNodesInVisibleOrder(): TreeNode[] {
return orderSelectedTreeNodes(visibleTreeNodes(), connectionStore.selectedTreeNodeIds);
}
function tableReferenceDragLabel(payload: QueryEditorTableReferencePayload): string { 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; return payload.tableName || payload.database;
} }
function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null { function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
if (!canDragTableReference.value) return 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") { if (activeNode.value.type === "database") {
return createTableReferencePayload({ return createTableReferencePayload({
connectionId: activeNode.value.connectionId, connectionId: activeNode.value.connectionId,
@@ -1189,16 +1218,24 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
}); });
} }
if (activeNode.value.type === "column") { if (activeNode.value.type === "column") {
const columnName = columnNameForDrag(activeNode.value); const columnNames = resolveSidebarColumnDragNames(activeNode.value, selectedNodes);
if (!activeNode.value.tableName || !columnName) return null; if (!activeNode.value.connectionId || activeNode.value.database == null || columnNames.length === 0) return null;
return createTableReferencePayload({ return createColumnReferencePayload({
connectionId: activeNode.value.connectionId, connectionId: activeNode.value.connectionId,
database: activeNode.value.database, database: activeNode.value.database,
schema: activeNode.value.schema, schema: activeNode.value.schema,
tableName: activeNode.value.tableName, columnNames,
columnName,
databaseType: currentDatabaseType(), 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({ const payload = createTableReferencePayload({
@@ -1209,15 +1246,13 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
databaseType: currentDatabaseType(), databaseType: currentDatabaseType(),
driverProfile: currentDriverProfile(), driverProfile: currentDriverProfile(),
}); });
if (payload && tableCopyOptions.includeTableSchema) {
payload.includeTableSchema = true;
payload.tableNameSeparator = tableCopyOptions.tableNameSeparator;
}
return payload; 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) { function startTableReferenceDrag(payload: QueryEditorTableReferencePayload) {
draggingTableReferencePayload = payload; draggingTableReferencePayload = payload;
setActiveTableReferencePayload(payload); setActiveTableReferencePayload(payload);
@@ -45,6 +45,8 @@ vi.mock("@/stores/settingsStore", () => ({
sidebarAllowHorizontalScroll: false, sidebarAllowHorizontalScroll: false,
sidebarHiddenTablePrefixes: [], sidebarHiddenTablePrefixes: [],
sidebarObjectInfoMode: "none", sidebarObjectInfoMode: "none",
sidebarCopyTableNameSeparator: "comma",
sidebarCopyTableNameIncludeSchema: false,
}, },
}), }),
})); }));
@@ -147,4 +149,56 @@ describe("TreeItem table reference dragging", () => {
expect(onDrop).not.toHaveBeenCalled(); 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");
});
}); });
@@ -112,7 +112,7 @@ describe("useSidebarTreeExportRuntime", () => {
const runtime = useSidebarTreeExportRuntime({ const runtime = useSidebarTreeExportRuntime({
activeNode, activeNode,
connectionStore: connectionStore as never, connectionStore: connectionStore as never,
settingsStore: {} as never, settingsStore: exportSettings() as never,
acceptedSelectionIds: () => null, acceptedSelectionIds: () => null,
}); });
@@ -156,15 +156,15 @@ describe("useSidebarTreeExportRuntime", () => {
expect(source).toContain('import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue"'); expect(source).toContain('import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue"');
expect(exportDataXlsx).toContain("await api.getColumns("); expect(exportDataXlsx).toContain("await api.getColumns(");
expect(exportDataXlsx).toContain("hasXlsxHeaderComments(columnInfos?.map((column) => column.comment))"); 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.indexOf("await showSidebarTreeXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(target, "xlsx"'));
}); });
it("falls back to field-name headers when column metadata is unavailable", () => { it("falls back to field-name headers when column metadata is unavailable", () => {
const exportDataXlsx = functionBody("exportDataXlsx"); 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("// Export still works with field-name headers when column metadata is unavailable.");
expect(exportDataXlsx).toContain('await exportTableData(target, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)'); expect(exportDataXlsx).toContain('await exportTableData(target, "xlsx"');
}); });
it("sends the selected mode's header overrides to both XLSX export paths", () => { it("sends the selected mode's header overrides to both XLSX export paths", () => {
@@ -246,4 +246,132 @@ describe("useSidebarTreeExportRuntime", () => {
expect.any(Function), 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));
});
}); });
@@ -4,15 +4,22 @@ import type { TreeNode } from "@/types/database";
import { useSidebarTreeToolRuntime } from "@/composables/useSidebarTreeToolRuntime"; 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 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({ const runtime = useSidebarTreeToolRuntime({
activeNode, activeNode,
connectionStore: connectionStore as never, connectionStore: connectionStore as never,
queryStore: {} as never, queryStore: {} as never,
settingsStore: {} as never, settingsStore: {} as never,
tableChildObjectName: () => "", tableChildObjectName: () => "",
acceptedSelectionIds: () => options.acceptedSelectionIds ?? null,
}); });
return { connectionStore, runtime }; return { connectionStore, runtime };
} }
@@ -43,3 +50,60 @@ describe("useSidebarTreeToolRuntime openDocs", () => {
expect(connectionStore.docsSource).toBeNull(); 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 diagramPrefillDatabase = ref("");
const diagramPrefillSchema = ref(""); const diagramPrefillSchema = ref("");
const diagramFocusTableName = ref(""); const diagramFocusTableName = ref("");
const diagramFocusTableNames = ref<string[]>([]);
const docsPrefillConnectionId = ref(""); const docsPrefillConnectionId = ref("");
const docsPrefillDatabase = ref(""); const docsPrefillDatabase = ref("");
const docsPrefillSchema = ref(""); const docsPrefillSchema = ref("");
@@ -183,6 +184,7 @@ export function useDialogSources() {
diagramPrefillDatabase.value = v.database; diagramPrefillDatabase.value = v.database;
diagramPrefillSchema.value = v.schema ?? ""; diagramPrefillSchema.value = v.schema ?? "";
diagramFocusTableName.value = v.tableName ?? ""; diagramFocusTableName.value = v.tableName ?? "";
diagramFocusTableNames.value = v.tableNames ?? (v.tableName ? [v.tableName] : []);
showDiagramDialog.value = true; showDiagramDialog.value = true;
connectionStore.diagramSource = null; connectionStore.diagramSource = null;
} }
@@ -511,6 +513,7 @@ export function useDialogSources() {
diagramPrefillDatabase, diagramPrefillDatabase,
diagramPrefillSchema, diagramPrefillSchema,
diagramFocusTableName, diagramFocusTableName,
diagramFocusTableNames,
docsPrefillConnectionId, docsPrefillConnectionId,
docsPrefillDatabase, docsPrefillDatabase,
docsPrefillSchema, docsPrefillSchema,
@@ -13,7 +13,7 @@ import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp"; import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
import { joinExportedDdls } from "@/lib/export/ddlExport"; import { joinExportedDdls } from "@/lib/export/ddlExport";
import { translateBackendError } from "@/i18n/backend-errors"; 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 { fetchTableDataForExport } from "@/lib/table/tableDataExport";
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue"; import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader"; import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
@@ -29,6 +29,7 @@ interface SidebarTreeExportRuntimeOptions {
} }
interface SidebarTableExportTarget { interface SidebarTableExportTarget {
nodeId: string;
connectionId: string; connectionId: string;
database: string; database: string;
schema?: string; schema?: string;
@@ -41,6 +42,44 @@ interface SidebarTableExportTarget {
identifierQuote?: string; identifierQuote?: string;
batchSize: number; batchSize: number;
rowLimit: number | null; 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) { export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOptions) {
@@ -80,6 +119,10 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
return sidebarStructureExportTargets(activeNode.value, connectionStore.treeNodes, options.acceptedSelectionIds() ?? connectionStore.selectedTreeNodeIds); 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() { async function exportStructure() {
const targets = structureExportTargets(); const targets = structureExportTargets();
if (!targets.length) return; if (!targets.length) return;
@@ -217,41 +260,56 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
} }
} }
async function exportDataLegacy(format: "json") { async function pickTableExportDirectory(): Promise<string | null> {
const node = activeNode.value; if (!isTauriRuntime()) return "";
if (!node.connectionId || !node.database) return; const { open } = await import("@tauri-apps/plugin-dialog");
const connectionId = node.connectionId; const selected = await open({ directory: true, multiple: false, title: t("contextMenu.exportDataSelectDirectory") });
const database = node.database; 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); const config = connectionStore.getConfig(connectionId);
if (!config) return; if (!config) return false;
try { try {
await connectionStore.ensureConnected(connectionId); 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 queryColumns = config.db_type === "neo4j" ? (await api.getColumns(connectionId, database, target.metadataSchema, target.tableName, target.catalog)).map((column) => column.name) : undefined;
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const result = await fetchTableDataForExport({ const result = await fetchTableDataForExport({
databaseType: effectiveDbType, databaseType: target.databaseType,
identifierQuote: connectionStore.connectionIdentifierQuote?.(connectionId), identifierQuote: target.identifierQuote,
schema: node.schema, schema: target.schema,
tableName: node.label, tableName: target.tableName,
tableType: node.tableType, tableType: target.tableType,
columns: queryColumns, columns: queryColumns,
executePage: (sql) => api.executeQuery(connectionId, database, sql), executePage: (sql) => api.executeQuery(connectionId, database, sql),
}); });
if (format === "json") { const outputPath = await resolveTableExportOutputPath(target, "json", outputDirectory);
let outputPath = `${node.label}.json`; if (!outputPath) return false;
if (isTauriRuntime()) { await api.exportQueryResultJson(outputPath, result.columns, result.rows);
const { save } = await import("@tauri-apps/plugin-dialog"); if (!suppressDoneToast) toast(t("grid.exported"));
const path = await save({ defaultPath: outputPath, filters: [{ name: "JSON", extensions: ["json"] }] }); return true;
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
toast(t("grid.exported"));
}
} catch (error: any) { } catch (error: any) {
toast(t("grid.exportFailed", { message: translateBackendError(t, error) }), 5000); toast(t("grid.exportFailed", { message: translateBackendError(t, error) }), 5000);
return false;
} }
} }
@@ -280,8 +338,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
}); });
} }
function currentTableExportTarget(): SidebarTableExportTarget | null { function tableExportTargetFromNode(node: TreeNode): SidebarTableExportTarget | null {
const node = activeNode.value;
if (!node.connectionId || !node.database) return null; if (!node.connectionId || !node.database) return null;
const connectionId = node.connectionId; const connectionId = node.connectionId;
const database = node.database; const database = node.database;
@@ -290,6 +347,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
const editorSettings = settingsStore.editorSettings; const editorSettings = settingsStore.editorSettings;
return { return {
nodeId: node.id,
connectionId, connectionId,
database, database,
schema: node.schema || undefined, 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; const { connectionId, database } = target;
let task: ExportTask | null = null; let task: ExportTask | null = null;
try { try {
// Choose the destination before registering a background task so cancellation creates no orphan tracker entry. const outputPath = await resolveTableExportOutputPath(target, format, outputDirectory);
let outputPath = `${target.tableName}.${format}`; if (!outputPath) return false;
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;
}
await connectionStore.ensureConnected(connectionId); await connectionStore.ensureConnected(connectionId);
task = addExportTask(target.tableName, format, outputPath); task = addExportTask(target.tableName, format, outputPath);
@@ -347,8 +404,8 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
currentTask.status = "Done"; currentTask.status = "Done";
currentTask.rowsExported = result.rows.length; currentTask.rowsExported = result.rows.length;
currentTask.totalRows = result.rows.length; currentTask.totalRows = result.rows.length;
toast(t("grid.exported")); if (!suppressDoneToast) toast(t("grid.exported"));
return; return true;
} }
const columnComments = const columnComments =
format === "xlsx" && exportColumnInfos format === "xlsx" && exportColumnInfos
@@ -378,41 +435,89 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
await api.startTableExport(request, (progress) => { await api.startTableExport(request, (progress) => {
updateTableExportTask(currentTask.exportId, progress); updateTableExportTask(currentTask.exportId, progress);
if (progress.status === "Done") toast(t("grid.exported")); if (progress.status === "Done") {
else if (progress.status === "Error") toast(t("grid.exportFailed", { message: translateBackendError(t, progress.errorMessage || "") }), 5000); 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) { } catch (error: any) {
if (task) { if (task) {
task.status = "Error"; task.status = "Error";
task.errorMessage = error?.message || String(error); task.errorMessage = error?.message || String(error);
} }
toast(t("grid.exportFailed", { message: translateBackendError(t, error) }), 5000); 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") { async function exportData(format: "csv" | "json" | "sql") {
if (format === "json") await exportDataLegacy(format); const targets = currentTableExportTargets();
else { if (!targets.length) return;
const target = currentTableExportTarget();
if (target) await exportTableData(target, format); 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() { async function exportDataXlsx() {
const target = currentTableExportTarget(); const targets = currentTableExportTargets();
if (!target) return; if (!targets.length) return;
let columnInfos: ColumnInfo[] | undefined; const columnInfosByTarget = new Map<string, ColumnInfo[] | undefined>();
try { let hasComments = false;
await connectionStore.ensureConnected(target.connectionId); for (const target of targets) {
columnInfos = await api.getColumns(target.connectionId, target.database, target.metadataSchema, target.tableName, target.catalog); let columnInfos: ColumnInfo[] | undefined;
} catch { try {
// Export still works with field-name headers when column metadata is unavailable. 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))); const xlsxOptions = await showSidebarTreeXlsxHeaderDialog(hasComments);
if (exportOptions === null) return; if (xlsxOptions === null) return;
await exportTableData(target, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter);
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 { return {
@@ -3,7 +3,7 @@ import type { useConnectionStore } from "@/stores/connectionStore";
import type { useQueryStore } from "@/stores/queryStore"; import type { useQueryStore } from "@/stores/queryStore";
import type { useSettingsStore } from "@/stores/settingsStore"; import type { useSettingsStore } from "@/stores/settingsStore";
import type { TreeNode } from "@/types/database"; import type { TreeNode } from "@/types/database";
import { allDatabasesExportSourceForNode, databaseExportSourceForNode } from "@/lib/sidebar/sidebarExportRuntime"; import { allDatabasesExportSourceForNode, databaseExportSourceForNode, sidebarSameSchemaStructureTargets } from "@/lib/sidebar/sidebarExportRuntime";
interface SidebarTreeToolRuntimeOptions { interface SidebarTreeToolRuntimeOptions {
activeNode: ShallowRef<TreeNode>; activeNode: ShallowRef<TreeNode>;
@@ -11,6 +11,7 @@ interface SidebarTreeToolRuntimeOptions {
queryStore: ReturnType<typeof useQueryStore>; queryStore: ReturnType<typeof useQueryStore>;
settingsStore: ReturnType<typeof useSettingsStore>; settingsStore: ReturnType<typeof useSettingsStore>;
tableChildObjectName: (node: TreeNode) => string; tableChildObjectName: (node: TreeNode) => string;
acceptedSelectionIds?: () => readonly string[] | null;
} }
export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions) { 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() { function openDiagram() {
const node = activeNode.value; const node = activeNode.value;
if (!node.connectionId || !node.database) return; 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 = { connectionStore.diagramSource = {
connectionId: node.connectionId, connectionId: node.connectionId,
database: node.database, database: node.database,
schema: node.schema, 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() { 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() { function openAllDatabasesExport() {
+8
View File
@@ -2945,6 +2945,7 @@ export default {
duplicateSelectedConnections: "Duplicate Selected {count} Connections", duplicateSelectedConnections: "Duplicate Selected {count} Connections",
newQuery: "New Query", newQuery: "New Query",
addToAi: "Add to AI", addToAi: "Add to AI",
addToAiMultiple: "Add to AI ({count} tables)",
instanceInfo: "Instance Info", instanceInfo: "Instance Info",
newSql: "New SQL", newSql: "New SQL",
generateSql: "Generate SQL", generateSql: "Generate SQL",
@@ -3011,6 +3012,9 @@ export default {
restoreSqliteDatabaseFailed: "Failed to restore SQLite database: {message}", restoreSqliteDatabaseFailed: "Failed to restore SQLite database: {message}",
importData: "Import Data", importData: "Import Data",
exportData: "Export Data", exportData: "Export Data",
exportDataMultiple: "Export Data ({count} tables)",
exportDataSelectDirectory: "Select export folder",
exportDataMultipleSuccess: "Exported {count} tables",
exportStructure: "Export Structure", exportStructure: "Export Structure",
exportStructurePreviewTitle: "Structure - {name}", exportStructurePreviewTitle: "Structure - {name}",
exportStructurePreviewTitleMultiple: "Structure - {count} objects", exportStructurePreviewTitleMultiple: "Structure - {count} objects",
@@ -6515,6 +6519,10 @@ export default {
sidebarHiddenTablePrefixes: "Hidden table name prefixes", 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.", 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_", 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", 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.", 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", sidebarTableSearchEnabled: "Enable table search inside databases",
+8
View File
@@ -2873,6 +2873,7 @@ export default withEnglishFallback({
instanceInfo: "Información de la instancia", instanceInfo: "Información de la instancia",
newQuery: "Nueva consulta", newQuery: "Nueva consulta",
addToAi: "Agregar a la IA", addToAi: "Agregar a la IA",
addToAiMultiple: "Agregar a la IA ({count} tablas)",
newSql: "Nuevo SQL", newSql: "Nuevo SQL",
generateSql: "Generar SQL", generateSql: "Generar SQL",
newInsert: "Nueva Inserción", newInsert: "Nueva Inserción",
@@ -2936,6 +2937,9 @@ export default withEnglishFallback({
restoreSqliteDatabaseFailed: "Error al restaurar la base de datos SQLite: {message}", restoreSqliteDatabaseFailed: "Error al restaurar la base de datos SQLite: {message}",
importData: "Importar datos", importData: "Importar datos",
exportData: "Exportar datos", exportData: "Exportar datos",
exportDataMultiple: "Exportar datos ({count} tablas)",
exportDataSelectDirectory: "Seleccionar carpeta de exportación",
exportDataMultipleSuccess: "Se exportaron {count} tablas",
exportStructure: "Exportar estructura", exportStructure: "Exportar estructura",
exportStructurePreviewTitle: "Estructura - {name}", exportStructurePreviewTitle: "Estructura - {name}",
exportStructurePreviewTitleMultiple: "Estructura - {count} objetos", exportStructurePreviewTitleMultiple: "Estructura - {count} objetos",
@@ -6186,6 +6190,10 @@ export default withEnglishFallback({
sidebarHiddenTablePrefixes: "Prefijos ocultos de tablas", 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.", 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_", 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", 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.", 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", sidebarTableSearchEnabled: "Activar búsqueda de tablas por base",
+8
View File
@@ -2870,6 +2870,7 @@ export default withEnglishFallback({
duplicateSelectedConnections: "Duplica {count} connessioni selezionate", duplicateSelectedConnections: "Duplica {count} connessioni selezionate",
newQuery: "Nuova Query", newQuery: "Nuova Query",
addToAi: "Aggiungi all'IA", addToAi: "Aggiungi all'IA",
addToAiMultiple: "Aggiungi all'IA ({count} tabelle)",
instanceInfo: "Informazioni Istanza", instanceInfo: "Informazioni Istanza",
newSql: "Nuovo SQL", newSql: "Nuovo SQL",
generateSql: "Genera SQL", generateSql: "Genera SQL",
@@ -2934,6 +2935,9 @@ export default withEnglishFallback({
restoreSqliteDatabaseFailed: "Impossibile ripristinare il database SQLite: {message}", restoreSqliteDatabaseFailed: "Impossibile ripristinare il database SQLite: {message}",
importData: "Importa Dati", importData: "Importa Dati",
exportData: "Esporta Dati", exportData: "Esporta Dati",
exportDataMultiple: "Esporta dati ({count} tabelle)",
exportDataSelectDirectory: "Seleziona cartella di esportazione",
exportDataMultipleSuccess: "Esportate {count} tabelle",
exportStructure: "Esporta Struttura", exportStructure: "Esporta Struttura",
exportStructurePreviewTitle: "Struttura - {name}", exportStructurePreviewTitle: "Struttura - {name}",
exportStructurePreviewTitleMultiple: "Struttura - {count} oggetti", exportStructurePreviewTitleMultiple: "Struttura - {count} oggetti",
@@ -6186,6 +6190,10 @@ export default withEnglishFallback({
sidebarHiddenTablePrefixes: "Prefissi dei nomi delle tabelle nascosti", 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.", 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_", 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", 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.", 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", sidebarTableSearchEnabled: "Abilita ricerca tabelle nel database",
+8
View File
@@ -2895,6 +2895,7 @@ export default withEnglishFallback({
duplicateSelectedConnections: "選択した{count}件の接続を複製", duplicateSelectedConnections: "選択した{count}件の接続を複製",
newQuery: "新しいクエリ", newQuery: "新しいクエリ",
addToAi: "AI に追加", addToAi: "AI に追加",
addToAiMultiple: "AI に追加({count} テーブル)",
newSql: "新しいSQL", newSql: "新しいSQL",
generateSql: "SQLを生成", generateSql: "SQLを生成",
newInsert: "新しいINSERT", newInsert: "新しいINSERT",
@@ -2958,6 +2959,9 @@ export default withEnglishFallback({
restoreSqliteDatabaseFailed: "SQLite データベースの復元に失敗しました: {message}", restoreSqliteDatabaseFailed: "SQLite データベースの復元に失敗しました: {message}",
importData: "データをインポート", importData: "データをインポート",
exportData: "データをエクスポート", exportData: "データをエクスポート",
exportDataMultiple: "データをエクスポート({count} テーブル)",
exportDataSelectDirectory: "エクスポート先フォルダを選択",
exportDataMultipleSuccess: "{count} テーブルをエクスポートしました",
exportStructure: "構造をエクスポート", exportStructure: "構造をエクスポート",
exportStructurePreviewTitle: "構造 - {name}", exportStructurePreviewTitle: "構造 - {name}",
exportStructurePreviewTitleMultiple: "構造 - {count}オブジェクト", exportStructurePreviewTitleMultiple: "構造 - {count}オブジェクト",
@@ -6212,6 +6216,10 @@ export default withEnglishFallback({
sidebarHiddenTablePrefixes: "非表示テーブル名プレフィックス", sidebarHiddenTablePrefixes: "非表示テーブル名プレフィックス",
sidebarHiddenTablePrefixesDescription: "1行に1つのプレフィックス。サイドバーのテーブル、ビュー、コレクションラベルのみ短縮されます。ツールチップと操作は完全な名前を使用します。", sidebarHiddenTablePrefixesDescription: "1行に1つのプレフィックス。サイドバーのテーブル、ビュー、コレクションラベルのみ短縮されます。ツールチップと操作は完全な名前を使用します。",
sidebarHiddenTablePrefixesPlaceholder: "Example:\nODS_\nT8Y2_LONG_", sidebarHiddenTablePrefixesPlaceholder: "Example:\nODS_\nT8Y2_LONG_",
sidebarCopyTableNameSeparator: "コピー時のテーブル名区切り",
sidebarCopyTableNameSeparatorDescription: "サイドバーで複数のテーブル名または列名をコピー、または SQL エディターにドラッグするときに使う区切り文字。",
sidebarCopyTableNameIncludeSchema: "コピーするテーブル名にスキーマを含める",
sidebarCopyTableNameIncludeSchemaDescription: "有効にすると、コピーまたはドラッグしたテーブル名は、アクティブな接続の識別子クォート規則に従った schema.table 形式になります。",
sidebarTablePageSize: "サイドバーのページサイズ", sidebarTablePageSize: "サイドバーのページサイズ",
sidebarTablePageSizeDescription: "サイドバーツリーで1ページあたりに読み込むテーブル/オブジェクトの最大数です。テーブルが多い場合は増やすとページ切り替えが減ります。", sidebarTablePageSizeDescription: "サイドバーツリーで1ページあたりに読み込むテーブル/オブジェクトの最大数です。テーブルが多い場合は増やすとページ切り替えが減ります。",
sidebarTableSearchEnabled: "データベース内のテーブル検索を有効化", sidebarTableSearchEnabled: "データベース内のテーブル検索を有効化",
+8
View File
@@ -2764,6 +2764,7 @@ export default withEnglishFallback({
duplicateSelectedConnections: "선택한 연결 {count}개 복제", duplicateSelectedConnections: "선택한 연결 {count}개 복제",
newQuery: "새 쿼리", newQuery: "새 쿼리",
addToAi: "AI에 추가", addToAi: "AI에 추가",
addToAiMultiple: "AI에 추가({count}개 테이블)",
instanceInfo: "인스턴스 정보", instanceInfo: "인스턴스 정보",
newSql: "새 SQL", newSql: "새 SQL",
generateSql: "SQL 생성", generateSql: "SQL 생성",
@@ -2830,6 +2831,9 @@ export default withEnglishFallback({
restoreSqliteDatabaseFailed: "SQLite 데이터베이스 복원 실패: {message}", restoreSqliteDatabaseFailed: "SQLite 데이터베이스 복원 실패: {message}",
importData: "데이터 가져오기", importData: "데이터 가져오기",
exportData: "데이터 내보내기", exportData: "데이터 내보내기",
exportDataMultiple: "데이터 내보내기 ({count}개 테이블)",
exportDataSelectDirectory: "보낼 폴더 선택",
exportDataMultipleSuccess: "{count}개 테이블을보냈습니다",
exportStructure: "구조 내보내기", exportStructure: "구조 내보내기",
exportStructurePreviewTitle: "구조 - {name}", exportStructurePreviewTitle: "구조 - {name}",
exportStructurePreviewTitleMultiple: "구조 - 객체 {count}개", exportStructurePreviewTitleMultiple: "구조 - 객체 {count}개",
@@ -5908,6 +5912,10 @@ export default withEnglishFallback({
sidebarHiddenTablePrefixes: "숨겨진 테이블 이름 접두사", sidebarHiddenTablePrefixes: "숨겨진 테이블 이름 접두사",
sidebarHiddenTablePrefixesDescription: "한 줄에 하나의 접두사. 사이드바의 테이블, 뷰, 컬렉션 라벨만 줄이며 툴팁과 작업은 전체 이름을 계속 사용합니다.", sidebarHiddenTablePrefixesDescription: "한 줄에 하나의 접두사. 사이드바의 테이블, 뷰, 컬렉션 라벨만 줄이며 툴팁과 작업은 전체 이름을 계속 사용합니다.",
sidebarHiddenTablePrefixesPlaceholder: "예:\nODS_\nT8Y2_LONG_", sidebarHiddenTablePrefixesPlaceholder: "예:\nODS_\nT8Y2_LONG_",
sidebarCopyTableNameSeparator: "테이블 이름 복사 구분자",
sidebarCopyTableNameSeparatorDescription: "사이드바에서 여러 테이블 또는 열 이름을 복사하거나 SQL 편집기로 끌어다 놓을 때 사용하는 구분자입니다.",
sidebarCopyTableNameIncludeSchema: "복사한 테이블 이름에 스키마 포함",
sidebarCopyTableNameIncludeSchemaDescription: "활성화하면 복사하거나 끌어다 놓은 테이블 이름이 활성 연결의 식별자 따옴표 규칙을 사용하는 스키마.테이블 형식이 됩니다.",
sidebarTablePageSize: "사이드바 페이지 크기", sidebarTablePageSize: "사이드바 페이지 크기",
sidebarTablePageSizeDescription: "사이드바 트리에서 페이지당 로드되는 최대 테이블/객체 수입니다. 테이블이 많고 페이지를 줄이려면 늘리세요.", sidebarTablePageSizeDescription: "사이드바 트리에서 페이지당 로드되는 최대 테이블/객체 수입니다. 테이블이 많고 페이지를 줄이려면 늘리세요.",
sidebarTableSearchEnabled: "데이터베이스 내 테이블 검색 활성화", sidebarTableSearchEnabled: "데이터베이스 내 테이블 검색 활성화",
+8
View File
@@ -2873,6 +2873,7 @@ export default withEnglishFallback({
instanceInfo: "Informações da Instância", instanceInfo: "Informações da Instância",
newQuery: "Nova Consulta", newQuery: "Nova Consulta",
addToAi: "Adicionar à IA", addToAi: "Adicionar à IA",
addToAiMultiple: "Adicionar à IA ({count} tabelas)",
newSql: "Novo SQL", newSql: "Novo SQL",
generateSql: "Gerar SQL", generateSql: "Gerar SQL",
newInsert: "Novo Insert", newInsert: "Novo Insert",
@@ -2936,6 +2937,9 @@ export default withEnglishFallback({
restoreSqliteDatabaseFailed: "Falha ao restaurar o banco de dados SQLite: {message}", restoreSqliteDatabaseFailed: "Falha ao restaurar o banco de dados SQLite: {message}",
importData: "Importar Dados", importData: "Importar Dados",
exportData: "Exportar Dados", exportData: "Exportar Dados",
exportDataMultiple: "Exportar dados ({count} tabelas)",
exportDataSelectDirectory: "Selecionar pasta de exportação",
exportDataMultipleSuccess: "Exportadas {count} tabelas",
exportStructure: "Exportar Estrutura", exportStructure: "Exportar Estrutura",
exportStructurePreviewTitle: "Estrutura - {name}", exportStructurePreviewTitle: "Estrutura - {name}",
exportStructurePreviewTitleMultiple: "Estrutura - {count} objetos", exportStructurePreviewTitleMultiple: "Estrutura - {count} objetos",
@@ -6188,6 +6192,10 @@ export default withEnglishFallback({
sidebarHiddenTablePrefixes: "Prefixos de nome de tabela ocultos", 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.", 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_", 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", 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.", 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", sidebarTableSearchEnabled: "Ativar busca de tabelas por banco",
+8
View File
@@ -2868,6 +2868,7 @@ export default withEnglishFallback({
openDamengJobAdmin: "打开达梦代理作业", openDamengJobAdmin: "打开达梦代理作业",
newQuery: "新建查询", newQuery: "新建查询",
addToAi: "添加到 AI", addToAi: "添加到 AI",
addToAiMultiple: "添加到 AI({count} 张表)",
instanceInfo: "实例信息", instanceInfo: "实例信息",
newInsert: "新建新增", newInsert: "新建新增",
newSql: "新建 SQL", newSql: "新建 SQL",
@@ -2934,6 +2935,9 @@ export default withEnglishFallback({
restoreSqliteDatabaseFailed: "还原 SQLite 数据库失败:{message}", restoreSqliteDatabaseFailed: "还原 SQLite 数据库失败:{message}",
importData: "导入数据", importData: "导入数据",
exportData: "导出数据", exportData: "导出数据",
exportDataMultiple: "导出数据({count} 张表)",
exportDataSelectDirectory: "选择导出文件夹",
exportDataMultipleSuccess: "已导出 {count} 张表",
exportStructure: "导出表结构", exportStructure: "导出表结构",
exportStructurePreviewTitle: "表结构 - {name}", exportStructurePreviewTitle: "表结构 - {name}",
exportStructurePreviewTitleMultiple: "表结构 - {count} 个对象", exportStructurePreviewTitleMultiple: "表结构 - {count} 个对象",
@@ -6498,6 +6502,10 @@ export default withEnglishFallback({
sidebarHiddenTablePrefixes: "隐藏表名前缀", sidebarHiddenTablePrefixes: "隐藏表名前缀",
sidebarHiddenTablePrefixesDescription: "每行一个前缀,仅影响侧边栏表、视图和集合的显示名称,悬浮提示和实际操作仍使用完整名称。", sidebarHiddenTablePrefixesDescription: "每行一个前缀,仅影响侧边栏表、视图和集合的显示名称,悬浮提示和实际操作仍使用完整名称。",
sidebarHiddenTablePrefixesPlaceholder: "例如:\nODS_\nT8Y2_LONG_", sidebarHiddenTablePrefixesPlaceholder: "例如:\nODS_\nT8Y2_LONG_",
sidebarCopyTableNameSeparator: "复制表名分隔符",
sidebarCopyTableNameSeparatorDescription: "多选侧边栏表或字段后复制名称或拖拽到 SQL 编辑器时使用的分隔符。",
sidebarCopyTableNameIncludeSchema: "复制表名包含 schema",
sidebarCopyTableNameIncludeSchemaDescription: "开启后复制或拖拽表名时使用 schema.table 限定名,并按当前连接方言自动加引号。",
sidebarTablePageSize: "侧边栏分页大小", sidebarTablePageSize: "侧边栏分页大小",
sidebarTablePageSizeDescription: "侧边栏树每页最多加载的表/对象数量。表多时可以调大减少翻页次数。", sidebarTablePageSizeDescription: "侧边栏树每页最多加载的表/对象数量。表多时可以调大减少翻页次数。",
sidebarTableSearchEnabled: "启用库下表搜索框", sidebarTableSearchEnabled: "启用库下表搜索框",
+8
View File
@@ -2869,6 +2869,7 @@ export default withEnglishFallback({
duplicateSelectedConnections: "複製選取的 {count} 個連線", duplicateSelectedConnections: "複製選取的 {count} 個連線",
newQuery: "建立查詢", newQuery: "建立查詢",
addToAi: "加入 AI", addToAi: "加入 AI",
addToAiMultiple: "加入 AI({count} 張表)",
instanceInfo: "實例資訊", instanceInfo: "實例資訊",
newSql: "新增 SQL", newSql: "新增 SQL",
generateSql: "產生 SQL", generateSql: "產生 SQL",
@@ -2933,6 +2934,9 @@ export default withEnglishFallback({
restoreSqliteDatabaseFailed: "還原 SQLite 資料庫失敗:{message}", restoreSqliteDatabaseFailed: "還原 SQLite 資料庫失敗:{message}",
importData: "匯入資料", importData: "匯入資料",
exportData: "匯出資料", exportData: "匯出資料",
exportDataMultiple: "匯出資料({count} 張表)",
exportDataSelectDirectory: "選擇匯出資料夾",
exportDataMultipleSuccess: "已匯出 {count} 張表",
exportStructure: "匯出資料表結構", exportStructure: "匯出資料表結構",
exportStructurePreviewTitle: "資料表結構 - {name}", exportStructurePreviewTitle: "資料表結構 - {name}",
exportStructurePreviewTitleMultiple: "資料表結構 - {count} 個物件", exportStructurePreviewTitleMultiple: "資料表結構 - {count} 個物件",
@@ -5504,6 +5508,10 @@ export default withEnglishFallback({
sidebarHiddenTablePrefixes: "隱藏資料表名稱字首", sidebarHiddenTablePrefixes: "隱藏資料表名稱字首",
sidebarHiddenTablePrefixesDescription: "每行一個字首。只縮短側邊欄中的資料表、檢視和集合標籤;工具提示和實際操作仍使用完整名稱。", sidebarHiddenTablePrefixesDescription: "每行一個字首。只縮短側邊欄中的資料表、檢視和集合標籤;工具提示和實際操作仍使用完整名稱。",
sidebarHiddenTablePrefixesPlaceholder: "範例:\nODS_\nT8Y2_LONG_", sidebarHiddenTablePrefixesPlaceholder: "範例:\nODS_\nT8Y2_LONG_",
sidebarCopyTableNameSeparator: "複製表名分隔符",
sidebarCopyTableNameSeparatorDescription: "多選側邊欄表或欄位後複製名稱或拖曳到 SQL 編輯器時使用的分隔符。",
sidebarCopyTableNameIncludeSchema: "複製表名包含 schema",
sidebarCopyTableNameIncludeSchemaDescription: "開啟後複製或拖曳表名時使用 schema.table 限定名,並依目前連線方言自動加引號。",
sidebarTablePageSize: "側邊欄分頁大小", sidebarTablePageSize: "側邊欄分頁大小",
sidebarTablePageSizeDescription: "側邊欄樹每頁最多載入的資料表/物件數量。資料表多時可調大減少翻頁次數。", sidebarTablePageSizeDescription: "側邊欄樹每頁最多載入的資料表/物件數量。資料表多時可調大減少翻頁次數。",
sidebarTableSearchEnabled: "啟用庫下資料表搜尋框", sidebarTableSearchEnabled: "啟用庫下資料表搜尋框",
@@ -12,4 +12,14 @@ describe("add to AI database context", () => {
expect(addToAiSource).toContain("else if (hasTreeNodeDatabaseContext(node))"); expect(addToAiSource).toContain("else if (hasTreeNodeDatabaseContext(node))");
expect(addToAiSource).not.toContain("else if (node.database)"); 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 { 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", () => { describe("query editor table reference drop", () => {
it("inserts a quoted database name for database references", () => { 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"], columnNames: ["id", "order no", "order", "created_at"],
databaseType: "mysql", 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({ const postgres = createColumnReferencePayload({
connectionId: "conn-1", connectionId: "conn-1",
@@ -110,7 +112,29 @@ describe("query editor table reference drop", () => {
columnNames: ["id", "OrderNo", "select"], columnNames: ["id", "OrderNo", "select"],
databaseType: "postgres", 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", () => { 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"], columnNames: ["id", "order no"],
databaseType: "oracle", 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", () => { 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"], columnNames: ["id", "order", "order no"],
databaseType: "sqlserver", 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", () => { 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", database: "app-db",
columnNames: ["order no"], 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 { describe, expect, it } from "vitest";
import type { TreeNode } from "@/types/database"; 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", () => { describe("sidebar export runtime", () => {
it("prepares database and table export sources", () => { 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], [third.id, first.id, second.id])).toEqual([first, second, third]);
expect(sidebarStructureExportTargets(first, [group], [second.id])).toEqual([first]); 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 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 { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier.ts"; 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_HOVER_EVENT = "dbx-table-reference-hover";
export const DBX_TABLE_REFERENCE_DRAG_END_EVENT = "dbx-table-reference-drag-end"; export const DBX_TABLE_REFERENCE_DRAG_END_EVENT = "dbx-table-reference-drag-end";
export interface QueryEditorTableReferenceEntry {
schema?: string;
tableName: string;
}
export interface QueryEditorTableReferencePayload { export interface QueryEditorTableReferencePayload {
kind: "dbx-table-reference"; kind: "dbx-table-reference";
connectionId: string; connectionId: string;
database: string; database: string;
schema?: string; schema?: string;
tableName?: string; tableName?: string;
tableReferences?: QueryEditorTableReferenceEntry[];
columnName?: string; columnName?: string;
/** 多个结果列一起拖入时按选择顺序排列;单列沿用 columnName。 */ /** 多个结果列一起拖入时按选择顺序排列;单列沿用 columnName。 */
columnNames?: string[]; columnNames?: string[];
referenceType?: "database" | "table" | "column"; referenceType?: "database" | "table" | "column";
databaseType?: DatabaseType; databaseType?: DatabaseType;
driverProfile?: string; driverProfile?: string;
columnNameSeparator?: ColumnNameCopySeparator;
tableNameSeparator?: ColumnNameCopySeparator;
includeTableSchema?: boolean;
} }
export interface QueryEditorTableReferenceDropDetail { export interface QueryEditorTableReferenceDropDetail {
@@ -62,16 +72,38 @@ export interface QueryEditorTableReferenceHoverDetail {
clientY: number; clientY: number;
} }
export interface TableReferenceInsertOptions {
tableNameSeparator?: ColumnNameCopySeparator;
includeTableSchema?: boolean;
columnNameSeparator?: ColumnNameCopySeparator;
}
function normalizeColumnNames(value: unknown): string[] { function normalizeColumnNames(value: unknown): string[] {
if (!Array.isArray(value)) return []; if (!Array.isArray(value)) return [];
return value.map((name) => (typeof name === "string" ? name.trim() : "")).filter((name) => name.length > 0); 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 各构造路径共用同一拷贝规则。 */ /** schema/databaseType/driverProfile 为可选元数据,payload 各构造路径共用同一拷贝规则。 */
function applyOptionalReferenceMeta(payload: QueryEditorTableReferencePayload, source: Partial<QueryEditorTableReferencePayload>) { function applyOptionalReferenceMeta(payload: QueryEditorTableReferencePayload, source: Partial<QueryEditorTableReferencePayload>) {
if (typeof source.schema === "string" && source.schema) payload.schema = source.schema; if (typeof source.schema === "string" && source.schema) payload.schema = source.schema;
if (source.databaseType) payload.databaseType = source.databaseType; if (source.databaseType) payload.databaseType = source.databaseType;
if (typeof source.driverProfile === "string" && source.driverProfile) payload.driverProfile = source.driverProfile; 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; let activeTableReferencePayload: QueryEditorTableReferencePayload | null = null;
@@ -107,7 +139,40 @@ export function createTableReferencePayload(options: {
return payload; 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); const columnNames = normalizeColumnNames(options.columnNames);
if (!options.connectionId || options.database == null || columnNames.length === 0) return null; if (!options.connectionId || options.database == null || columnNames.length === 0) return null;
const payload: QueryEditorTableReferencePayload = { 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; if (typeof parsed.driverProfile === "string" && parsed.driverProfile) payload.driverProfile = parsed.driverProfile;
return payload; return payload;
} }
if (typeof parsed.tableName !== "string" || !parsed.tableName) { const columnNames = normalizeColumnNames(parsed.columnNames);
// 无来源表的纯列引用(如从查询结果列头拖入),tableName 非必需。 if (columnNames.length > 0 && (parsed.referenceType === "column" || !parsed.tableName)) {
const columnNames = normalizeColumnNames(parsed.columnNames);
if (columnNames.length === 0) return null;
const payload: QueryEditorTableReferencePayload = { const payload: QueryEditorTableReferencePayload = {
kind: "dbx-table-reference", kind: "dbx-table-reference",
connectionId: parsed.connectionId, connectionId: parsed.connectionId,
@@ -157,18 +220,34 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu
applyOptionalReferenceMeta(payload, parsed); applyOptionalReferenceMeta(payload, parsed);
return payload; 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 columnName = typeof parsed.columnName === "string" && parsed.columnName ? parsed.columnName : undefined;
const referenceType = parsed.referenceType === "column" || columnName ? "column" : "table"; const referenceType = parsed.referenceType === "column" || columnName || columnNames.length > 0 ? "column" : "table";
if (referenceType === "column" && !columnName) return null;
const payload: QueryEditorTableReferencePayload = { const payload: QueryEditorTableReferencePayload = {
kind: "dbx-table-reference", kind: "dbx-table-reference",
connectionId: parsed.connectionId, connectionId: parsed.connectionId,
database: parsed.database, database: parsed.database,
tableName: parsed.tableName, tableName: parsed.tableName,
}; };
if (referenceType === "column" && columnName) { if (referenceType === "column") {
payload.columnName = columnName;
payload.referenceType = "column"; payload.referenceType = "column";
if (columnNames.length > 0) payload.columnNames = columnNames;
else if (columnName) payload.columnName = columnName;
else return null;
} }
applyOptionalReferenceMeta(payload, parsed); applyOptionalReferenceMeta(payload, parsed);
return payload; return payload;
@@ -211,14 +290,40 @@ export function createTableReferenceDragEndEvent(): Event {
return new Event(DBX_TABLE_REFERENCE_DRAG_END_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; const databaseType = payload.databaseType ?? fallbackDatabaseType;
if (payload.referenceType === "database") { if (payload.referenceType === "database") {
return quoteTableIdentifier(databaseType, payload.database); return quoteTableIdentifier(databaseType, payload.database);
} }
const columnNames = payload.columnNames?.length ? payload.columnNames : payload.columnName ? [payload.columnName] : []; const columnNames = payload.columnNames?.length ? payload.columnNames : payload.columnName ? [payload.columnName] : [];
if (payload.referenceType === "column" && columnNames.length > 0) { 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; const tableName = payload.tableName || payload.database;
return qualifiedTableName({ return qualifiedTableName({
@@ -80,6 +80,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"sidebarIndent", "sidebarIndent",
"sidebarFontSize", "sidebarFontSize",
"sidebarHiddenTablePrefixes", "sidebarHiddenTablePrefixes",
"sidebarCopyTableNameSeparator",
"sidebarCopyTableNameIncludeSchema",
"redisKeyTemplates", "redisKeyTemplates",
"exportBatchSize", "exportBatchSize",
"exportRowLimitEnabled", "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 type { TreeNode } from "@/types/database";
import { sidebarDdlTargetsForExecutionContext } from "@/lib/sidebar/sidebarDdlTemplate";
export interface SidebarDatabaseExportSource { export interface SidebarDatabaseExportSource {
connectionId: string; connectionId: string;
@@ -25,15 +26,26 @@ export function allDatabasesExportSourceForNode(node: TreeNode): SidebarDatabase
return { connectionId: node.connectionId, database: "", allDatabases: true }; return { connectionId: node.connectionId, database: "", allDatabases: true };
} }
export function sidebarStructureExportTargets(activeNode: TreeNode, treeNodes: readonly TreeNode[], selectedNodeIds: readonly string[]): Array<TreeNode & { connectionId: string; database: string }> { export type SidebarStructureExportTarget = 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 []; 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 selected = new Set(selectedNodeIds);
const targets: Array<TreeNode & { connectionId: string; database: string }> = []; const targets: SidebarStructureExportTarget[] = [];
const visit = (nodes: readonly TreeNode[]) => { const visit = (nodes: readonly TreeNode[]) => {
for (const node of nodes) { 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.children) visit(node.children);
if (node.hiddenChildren) visit(node.hiddenChildren); if (node.hiddenChildren) visit(node.hiddenChildren);
} }
@@ -41,3 +53,23 @@ export function sidebarStructureExportTargets(activeNode: TreeNode, treeNodes: r
visit(treeNodes); visit(treeNodes);
return targets.length > 1 && targets.some((node) => node.id === activeNode.id) ? targets : [activeNode]; 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; handleRowKeydown(node: TreeNode, event: KeyboardEvent): void;
openPrimaryVisibleFilter(node: TreeNode): void; openPrimaryVisibleFilter(node: TreeNode): void;
openDataInNewTab(node: TreeNode): void; openDataInNewTab(node: TreeNode): void;
openDdlForSelection(node: TreeNode, selectedNodeIds: readonly string[]): Promise<boolean>;
requestPaste(node: TreeNode): boolean; requestPaste(node: TreeNode): boolean;
toggleNode(node: TreeNode): void; toggleNode(node: TreeNode): void;
} }
@@ -518,6 +518,7 @@ export const useConnectionStore = defineStore("connection", () => {
database: string; database: string;
schema?: string; schema?: string;
tableName?: string; tableName?: string;
tableNames?: string[];
} | null>(null); } | null>(null);
const docsSource = ref<{ const docsSource = ref<{
connectionId: string; connectionId: string;
+11 -1
View File
@@ -15,8 +15,10 @@ import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/co
import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry"; import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort"; 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 { 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 type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion"; import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
@@ -670,6 +672,8 @@ export interface EditorSettings {
formatSqlOnSqlFileSave: boolean; formatSqlOnSqlFileSave: boolean;
updateNotificationsEnabled: boolean; updateNotificationsEnabled: boolean;
sidebarHiddenTablePrefixes: string[]; sidebarHiddenTablePrefixes: string[];
sidebarCopyTableNameSeparator: ColumnNameCopySeparator;
sidebarCopyTableNameIncludeSchema: boolean;
sidebarObjectInfoMode: SidebarObjectInfoMode; sidebarObjectInfoMode: SidebarObjectInfoMode;
sidebarShowConnectionNotes: boolean; sidebarShowConnectionNotes: boolean;
sidebarAllowHorizontalScroll: boolean; sidebarAllowHorizontalScroll: boolean;
@@ -888,6 +892,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
formatSqlOnSqlFileSave: false, formatSqlOnSqlFileSave: false,
updateNotificationsEnabled: true, updateNotificationsEnabled: true,
sidebarHiddenTablePrefixes: [], sidebarHiddenTablePrefixes: [],
sidebarCopyTableNameSeparator: "comma",
sidebarCopyTableNameIncludeSchema: false,
sidebarObjectInfoMode: "comment-inline", sidebarObjectInfoMode: "comment-inline",
sidebarShowConnectionNotes: false, sidebarShowConnectionNotes: false,
sidebarAllowHorizontalScroll: false, sidebarAllowHorizontalScroll: false,
@@ -1322,6 +1328,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
formatSqlOnSqlFileSave: settings.formatSqlOnSqlFileSave === true, formatSqlOnSqlFileSave: settings.formatSqlOnSqlFileSave === true,
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled, updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes), sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
sidebarCopyTableNameSeparator: normalizeSidebarCopyTableNameSeparator(settings.sidebarCopyTableNameSeparator),
sidebarCopyTableNameIncludeSchema: settings.sidebarCopyTableNameIncludeSchema === true,
sidebarObjectInfoMode: normalizeSidebarObjectInfoMode( sidebarObjectInfoMode: normalizeSidebarObjectInfoMode(
settings.sidebarObjectInfoMode, settings.sidebarObjectInfoMode,
( (
@@ -1925,6 +1933,8 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.formatSqlOnSqlFileSave !== undefined) editorSettings.value.formatSqlOnSqlFileSave = partial.formatSqlOnSqlFileSave === true; if (partial.formatSqlOnSqlFileSave !== undefined) editorSettings.value.formatSqlOnSqlFileSave = partial.formatSqlOnSqlFileSave === true;
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled; if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes); 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.sidebarObjectInfoMode !== undefined) editorSettings.value.sidebarObjectInfoMode = normalizeSidebarObjectInfoMode(partial.sidebarObjectInfoMode);
if (partial.sidebarShowConnectionNotes !== undefined) editorSettings.value.sidebarShowConnectionNotes = partial.sidebarShowConnectionNotes === true; if (partial.sidebarShowConnectionNotes !== undefined) editorSettings.value.sidebarShowConnectionNotes = partial.sidebarShowConnectionNotes === true;
if (partial.sidebarAllowHorizontalScroll !== undefined) editorSettings.value.sidebarAllowHorizontalScroll = partial.sidebarAllowHorizontalScroll; if (partial.sidebarAllowHorizontalScroll !== undefined) editorSettings.value.sidebarAllowHorizontalScroll = partial.sidebarAllowHorizontalScroll;
+23 -2
View File
@@ -88,7 +88,7 @@ test("table copy menu uses the shared single and multi-selection clipboard path"
assert.match(copySelectedNamesBody, /const selectedNodes = selectedTreeNodesInVisibleOrder\(\)/); assert.match(copySelectedNamesBody, /const selectedNodes = selectedTreeNodesInVisibleOrder\(\)/);
assert.match(copySelectedNamesBody, /selectedNodes\.length > 1 && selectedNodes\.some\(\(node\) => node\.id === activeNode\.value\.id\) \? selectedNodes : \[activeNode\.value\]/); assert.match(copySelectedNamesBody, /selectedNodes\.length > 1 && selectedNodes\.some\(\(node\) => node\.id === activeNode\.value\.id\) \? selectedNodes : \[activeNode\.value\]/);
assert.match(copySelectedNamesBody, /updateTreeClipboardForNodes\(nodes\)/); assert.match(copySelectedNamesBody, /updateTreeClipboardForNodes\(nodes\)/);
assert.match(copySelectedNamesBody, /copyToClipboard\(nodes\.map\(copyNameForTreeNode\)\.join\("\\n"\)\)/); assert.match(copySelectedNamesBody, /formatSelectedTableNamesForClipboard/);
}); });
test("MySQL object name menus expose leaf and display-path copy choices", () => { test("MySQL object name menus expose leaf and display-path copy choices", () => {
@@ -100,7 +100,7 @@ test("MySQL object name menus expose leaf and display-path copy choices", () =>
const databaseMenuBody = functionBody(runtimeHost, "buildDatabaseSidebarMenu"); const databaseMenuBody = functionBody(runtimeHost, "buildDatabaseSidebarMenu");
const objectMenuBody = functionBody(runtimeHost, "buildObjectSidebarMenu"); const objectMenuBody = functionBody(runtimeHost, "buildObjectSidebarMenu");
assert.match(copyNameBody, /copyNameForTreeNode\(node\)/); assert.match(copyNameBody, /formatSelectedTableNamesForClipboard/);
assert.match(copyDisplayPathBody, /copyDisplayPathForTreeNode\(node, connectionName\)/); assert.match(copyDisplayPathBody, /copyDisplayPathForTreeNode\(node, connectionName\)/);
assert.match(copyNameMenuItemBody, /currentDatabaseType\(\) === "mysql"/); assert.match(copyNameMenuItemBody, /currentDatabaseType\(\) === "mysql"/);
assert.match(copyNameMenuItemBody, /children: \[/); assert.match(copyNameMenuItemBody, /children: \[/);
@@ -114,6 +114,27 @@ test("MySQL object name menus expose leaf and display-path copy choices", () =>
assert.match(objectMenuBody, /node\.type === "sequence"[\s\S]*action: copyName/); assert.match(objectMenuBody, /node\.type === "sequence"[\s\S]*action: copyName/);
}); });
test("multi-select view ddl opens a combined ddl tab instead of export structure", () => {
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
const openDdlBody = functionBody(runtimeHost, "openDdl");
assert.doesNotMatch(openDdlBody, /exportStructure\(\)/);
assert.match(openDdlBody, /openSidebarMultiTableDdlTab\(targets\)/);
assert.match(runtimeHost, /openDdlForSelection/);
});
test("multi-select add-to-ai only mentions tables in the active execution context", () => {
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
// The function's return type annotation contains braces, so extract the body
// from the signature line to the next column-0 closing brace.
const selectedAiTableTargetsBody = /function selectedAiTableTargets\([^)]*\)[^\n]*\{[\s\S]*?\n\}/.exec(runtimeHost)?.[0] ?? "";
assert.notEqual(selectedAiTableTargetsBody, "");
assert.match(selectedAiTableTargetsBody, /resolveSidebarDdlTargets\(/);
assert.match(selectedAiTableTargetsBody, /target\.type === "table"/);
assert.doesNotMatch(selectedAiTableTargetsBody, /sidebarStructureExportTargets\(/);
});
test("successful tree table paste consumes only the clipboard used to start it", () => { test("successful tree table paste consumes only the clipboard used to start it", () => {
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8"); const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
const confirmPasteTableBody = functionBody(runtimeHost, "confirmPasteTable"); const confirmPasteTableBody = functionBody(runtimeHost, "confirmPasteTable");