feat(sidebar): batch actions for multi selected nodes
This commit is contained in:
1 parent
2f71a1a8aa
commit
69a032ed63
37 files changed
+1302
-159
No files matched your search
@@ -944,8 +944,10 @@ function sendSelectionToAi(sql: string) {
|
|||||||
|
|
||||||
let addToAiRequestId = 0;
|
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);
|
||||||
|
|||||||
+54
@@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+133
-5
@@ -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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+66
-2
@@ -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() {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: "データベース内のテーブル検索を有効化",
|
||||||
|
|||||||
@@ -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: "데이터베이스 내 테이블 검색 활성화",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: "启用库下表搜索框",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
Reference in new issue
Block a user