feat(sidebar): manage saved SQL by database
This commit is contained in:
1 parent
1efb3de113
commit
5f60fbe196
40 files changed
+1631
-150
No files matched your search
@@ -18,9 +18,11 @@ import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus";
|
||||
import { savedSqlFolderBranchFileCount } from "@/lib/savedSql/savedSqlFolderCounts";
|
||||
import { collectSavedSqlDirectoryImportFiles } from "@/lib/savedSql/savedSqlDirectoryImport";
|
||||
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
||||
import { ensureSqlExtension, stripSqlExtension } from "@/lib/savedSql/savedSqlFileName";
|
||||
import { savedSqlImportTarget } from "@/lib/savedSql/savedSqlImportTarget";
|
||||
import { savedSqlExecutionTargetFromTab, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
import { exportSavedSqlFileContent } from "@/lib/savedSql/savedSqlExport";
|
||||
import { orderedListRangeAnchorIndex, orderedListSelectionIntent } from "@/lib/selection/orderedListSelection";
|
||||
import { resolveExternalSqlFileTarget, unassociatedExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
import type { SavedSqlFile, SavedSqlFolder } from "@/types/database";
|
||||
@@ -74,7 +76,8 @@ function sanitizeFileSystemSegment(name: string) {
|
||||
|
||||
function uniqueImportedName(name: string, takenNames: Set<string>) {
|
||||
const normalized = ensureSqlExtension(name);
|
||||
if (!takenNames.has(normalized)) {
|
||||
const normalizedTakenNames = new Set([...takenNames].map((takenName) => ensureSqlExtension(takenName).toLocaleLowerCase()));
|
||||
if (!normalizedTakenNames.has(normalized.toLocaleLowerCase())) {
|
||||
takenNames.add(normalized);
|
||||
return normalized;
|
||||
}
|
||||
@@ -83,7 +86,7 @@ function uniqueImportedName(name: string, takenNames: Set<string>) {
|
||||
let counter = 2;
|
||||
while (true) {
|
||||
const candidate = `${base} (${counter}).sql`;
|
||||
if (!takenNames.has(candidate)) {
|
||||
if (!normalizedTakenNames.has(candidate.toLocaleLowerCase())) {
|
||||
takenNames.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
@@ -91,34 +94,12 @@ function uniqueImportedName(name: string, takenNames: Set<string>) {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadText(content: string, fileName: string) {
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function exportSingleFile(file: SavedSqlFile) {
|
||||
try {
|
||||
const loadedFile = await savedSqlStore.ensureFileContent(file.id);
|
||||
if (!loadedFile) return;
|
||||
const defaultFileName = sanitizeFileSystemSegment(ensureSqlExtension(file.name));
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({
|
||||
defaultPath: defaultFileName,
|
||||
filters: [{ name: "SQL", extensions: ["sql"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
await writeTextFile(path, loadedFile.sql);
|
||||
} else {
|
||||
await downloadText(loadedFile.sql, defaultFileName);
|
||||
}
|
||||
toast(t("sqlLibrary.exported"), 2000);
|
||||
const result = await exportSavedSqlFileContent(loadedFile.sql, file.name);
|
||||
if (result === "saved") toast(t("sqlLibrary.exported"), 2000);
|
||||
} catch (e: any) {
|
||||
toast(t("sqlLibrary.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
@@ -263,7 +244,8 @@ async function importDirectoryIntoLibrary(targetFolder?: SavedSqlFolder) {
|
||||
|
||||
toast(t("sqlLibrary.imported", { count: importFiles.length }), 2500);
|
||||
} catch (e: any) {
|
||||
toast(t("sqlLibrary.importFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
const message = e?.code === "SAVED_SQL_NAME_CONFLICT" ? savedSqlErrorMessage(e, t) : externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params));
|
||||
toast(t("sqlLibrary.importFailed", { message }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,15 +409,19 @@ async function openNewQueryInFolder(folder?: SavedSqlFolder) {
|
||||
|
||||
const takenNames = folder ? new Set(savedSqlStore.filesInFolder(folder.id).map((f) => f.name)) : new Set(savedSqlStore.filesWithoutFolder().map((f) => f.name));
|
||||
const name = uniqueImportedName("new_query.sql", takenNames);
|
||||
const file = await savedSqlStore.saveFile({
|
||||
connectionId,
|
||||
folderId: folder?.id,
|
||||
name,
|
||||
database: "",
|
||||
sql: "",
|
||||
});
|
||||
const tabId = queryStore.openSavedSql(file);
|
||||
connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId;
|
||||
try {
|
||||
const file = await savedSqlStore.saveFile({
|
||||
connectionId,
|
||||
folderId: folder?.id,
|
||||
name,
|
||||
database: "",
|
||||
sql: "",
|
||||
});
|
||||
const tabId = queryStore.openSavedSql(file);
|
||||
connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId;
|
||||
} catch (error) {
|
||||
toast(t("savedSql.saveFailed", { message: savedSqlErrorMessage(error, t) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch selection state
|
||||
@@ -631,7 +617,11 @@ async function confirmRename() {
|
||||
if (type === "folder") {
|
||||
await savedSqlStore.renameFolder(id, name);
|
||||
} else {
|
||||
await savedSqlStore.renameFile(id, ensureSqlExtension(name));
|
||||
try {
|
||||
await savedSqlStore.renameFile(id, ensureSqlExtension(name));
|
||||
} catch (error) {
|
||||
toast(t("savedSql.renameFailed", { message: savedSqlErrorMessage(error, t) }), 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,9 +678,13 @@ async function executeBatchDelete() {
|
||||
async function moveFilesToFolder(fileIds: string[], folderId?: string) {
|
||||
const movableIds = [...new Set(fileIds)].filter((id) => savedSqlStore.getFile(id));
|
||||
if (movableIds.length === 0) return;
|
||||
await savedSqlStore.moveFilesToFolder(movableIds, folderId);
|
||||
clearSelection();
|
||||
toast(t("sqlLibrary.moveSuccess", { count: movableIds.length }), 2000);
|
||||
try {
|
||||
await savedSqlStore.moveFilesToFolder(movableIds, folderId);
|
||||
clearSelection();
|
||||
toast(t("sqlLibrary.moveSuccess", { count: movableIds.length }), 2000);
|
||||
} catch (error) {
|
||||
toast(t("sqlLibrary.moveFailed", { message: savedSqlErrorMessage(error, t) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function openFile(file: SavedSqlFile, targetMode?: SavedSqlOpenTargetMode) {
|
||||
@@ -1045,7 +1039,7 @@ function onDocumentMouseUp() {
|
||||
const dropPromise = hadActiveDrag ? performDrop() : Promise.resolve();
|
||||
if (hadActiveDrag) markSuppressedClick();
|
||||
resetDragState();
|
||||
void dropPromise;
|
||||
void dropPromise.catch((error) => toast(t("sqlLibrary.moveFailed", { message: savedSqlErrorMessage(error, t) }), 5000));
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", onDocumentMouseMove, true);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { Search, X, ListFilter, ListOrdered, ArrowDownAZ, ArrowUpZA, CircleDot, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw, Loader2, Unplug } from "@lucide/vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { ObjectSourceKind, TableInfo, TableNameFilter, TreeNode, TreeNodeType } from "@/types/database";
|
||||
@@ -17,7 +18,7 @@ import { supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilit
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
|
||||
import { usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import { isInternalDorisCatalog, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import { connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget, type SidebarNodeScrollAlign } from "@/lib/sidebar/sidebarActiveTabTarget";
|
||||
import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/sql/queryCursorTableTarget";
|
||||
@@ -57,6 +58,7 @@ import { disconnectSidebarConnections } from "@/lib/sidebar/sidebarConnectionDis
|
||||
const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const searchQuery = ref("");
|
||||
@@ -961,7 +963,7 @@ function bindSidebarTreeRuntimeHost(host: Element | ComponentPublicInstance | nu
|
||||
sidebarTreeRuntime.bindHost(runtimeHost);
|
||||
}
|
||||
|
||||
const pendingRenameGroupId = ref<string | null>(null);
|
||||
const pendingRenameNodeId = ref<string | null>(null);
|
||||
const highlightedNodeId = ref<string | null>(null);
|
||||
let highlightTimer: number | undefined;
|
||||
|
||||
@@ -1026,7 +1028,7 @@ async function createNewGroup() {
|
||||
}
|
||||
|
||||
async function startRenamingCreatedGroup(groupId: string) {
|
||||
pendingRenameGroupId.value = groupId;
|
||||
pendingRenameNodeId.value = groupId;
|
||||
store.selectedTreeNodeId = groupId;
|
||||
if (isRootListPartial.value) {
|
||||
searchQuery.value = "";
|
||||
@@ -1039,11 +1041,22 @@ async function startRenamingCreatedGroup(groupId: string) {
|
||||
store.selectedTreeNodeId = groupId;
|
||||
}
|
||||
|
||||
async function startRenamingSavedSqlNode(nodeId: string) {
|
||||
pendingRenameNodeId.value = nodeId;
|
||||
store.selectedTreeNodeId = nodeId;
|
||||
store.selectedTreeNodeIds = [nodeId];
|
||||
await scrollToSidebarNode(nodeId);
|
||||
store.selectedTreeNodeId = nodeId;
|
||||
}
|
||||
|
||||
async function locateActiveTabInSidebar() {
|
||||
const tab = activeTab.value;
|
||||
if (!tab) return;
|
||||
|
||||
const connId = tab.connectionId;
|
||||
const tabTarget = activeTabSidebarTarget(tab);
|
||||
const locatesSavedSql = tabTarget?.type === "saved-sql-file";
|
||||
const savedSqlFile = locatesSavedSql ? savedSqlStore.getFile(tabTarget.savedSqlId) : undefined;
|
||||
const connId = savedSqlFile?.connectionId ?? tab.connectionId;
|
||||
|
||||
// Reconnect if the connection was disconnected (children are cleared on disconnect)
|
||||
if (connId && !store.connectedIds.has(connId)) {
|
||||
@@ -1057,13 +1070,24 @@ async function locateActiveTabInSidebar() {
|
||||
}
|
||||
|
||||
const config = connId ? store.getConfig(connId) : undefined;
|
||||
const cursorCandidate = queryCursorTableCandidate(tab, effectiveDatabaseTypeForConnection(config));
|
||||
const fallbackTarget = queryContextTargetFromCandidate(tab, cursorCandidate) ?? activeTabSidebarTarget(tab);
|
||||
const cursorCandidate = locatesSavedSql ? null : queryCursorTableCandidate(tab, effectiveDatabaseTypeForConnection(config));
|
||||
const fallbackTarget = locatesSavedSql ? tabTarget : (queryContextTargetFromCandidate(tab, cursorCandidate) ?? tabTarget);
|
||||
const initialTarget = cursorCandidate ? tableTargetFromCandidate(cursorCandidate) : fallbackTarget;
|
||||
if (!initialTarget) return;
|
||||
|
||||
// Ensure the tree is loaded deep enough to contain the preferred target.
|
||||
await ensureTreeLoadedForTarget(initialTarget);
|
||||
// Saved SQL rows live below their database's runtime Queries node. Loading
|
||||
// the database context first also makes explicit locate work after reconnect.
|
||||
const treeLoadTarget: ActiveTabSidebarTarget =
|
||||
locatesSavedSql && savedSqlFile?.connectionId && savedSqlFile.database
|
||||
? {
|
||||
type: "query-context",
|
||||
connectionId: savedSqlFile.connectionId,
|
||||
catalog: savedSqlFile.catalog,
|
||||
database: savedSqlFile.database,
|
||||
}
|
||||
: initialTarget;
|
||||
await ensureTreeLoadedForTarget(treeLoadTarget);
|
||||
|
||||
// Clear any active search filter so the node is visible
|
||||
if (isRootListPartial.value) {
|
||||
@@ -1075,12 +1099,12 @@ async function locateActiveTabInSidebar() {
|
||||
|
||||
let target = resolveLoadedLocateTarget(initialTarget, cursorCandidate);
|
||||
let nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null;
|
||||
if (!nodePath) {
|
||||
if (!nodePath && !locatesSavedSql) {
|
||||
// The first load may have served a stale schema cache whose async refresh
|
||||
// replaced the database node before its tables finished loading, so the
|
||||
// table isn't in the tree yet. Force a synchronous reload and retry once so
|
||||
// locate reaches the table, not just the database (issue #715).
|
||||
await ensureTreeLoadedForTarget(initialTarget, { force: true });
|
||||
await ensureTreeLoadedForTarget(treeLoadTarget, { force: true });
|
||||
target = resolveLoadedLocateTarget(initialTarget, cursorCandidate);
|
||||
nodePath = target ? findNodePathForTarget(target, store.treeNodes) : null;
|
||||
}
|
||||
@@ -1180,8 +1204,21 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
|
||||
if (config.db_type === "mq" || config.db_type === "nacos" || config.db_type === "consul") return;
|
||||
if (!("database" in target) || !target.database) return;
|
||||
|
||||
const usesExactCatalogScope = target.type === "query-context";
|
||||
const targetCatalog = usesExactCatalogScope ? target.catalog : undefined;
|
||||
if (usesExactCatalogScope) {
|
||||
const catalogNode = findDorisCatalogNode(store.treeNodes, connId, targetCatalog);
|
||||
if (catalogNode && (force || !catalogNode.children || catalogNode.children.length === 0)) {
|
||||
try {
|
||||
await store.loadDorisCatalogDatabases(catalogNode, loadOptions);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the database node
|
||||
const dbNode = findDatabaseNode(store.treeNodes, connId, target.database);
|
||||
const dbNode = findDatabaseNode(store.treeNodes, connId, target.database, targetCatalog, usesExactCatalogScope);
|
||||
if (!dbNode) return;
|
||||
const targetSchema = "schema" in target ? target.schema : undefined;
|
||||
const databaseChildrenLoaded = !!dbNode.children && dbNode.children.length > 0;
|
||||
@@ -1250,13 +1287,28 @@ function sameTreeName(left: string | undefined, right: string | undefined): bool
|
||||
return (left || "").toLowerCase() === (right || "").toLowerCase();
|
||||
}
|
||||
|
||||
function findDatabaseNode(nodes: TreeNode[], connId: string, database: string): TreeNode | null {
|
||||
function findDorisCatalogNode(nodes: TreeNode[], connId: string, catalog: string | undefined): TreeNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "database" && node.connectionId === connId && sameTreeName(node.database, database)) {
|
||||
if (node.type === "doris-catalog" && node.connectionId === connId) {
|
||||
const matches = catalog ? sameTreeName(node.catalog, catalog) : isInternalDorisCatalog(node.catalogType, node.catalog);
|
||||
if (matches) return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findDorisCatalogNode(node.children, connId, catalog);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findDatabaseNode(nodes: TreeNode[], connId: string, database: string, catalog?: string, exactCatalog = false): TreeNode | null {
|
||||
for (const node of nodes) {
|
||||
const catalogMatches = !exactCatalog || (catalog ? sameTreeName(node.catalog, catalog) : !node.catalog);
|
||||
if (node.type === "database" && node.connectionId === connId && sameTreeName(node.database, database) && catalogMatches) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findDatabaseNode(node.children, connId, database);
|
||||
const found = findDatabaseNode(node.children, connId, database, catalog, exactCatalog);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
@@ -1822,6 +1874,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
@open-visible-nacos-namespaces="openSidebarVisibleNacosNamespaces"
|
||||
@open-table-name-filters="openSidebarTableNameFilters"
|
||||
@request-group-rename="startRenamingCreatedGroup"
|
||||
@request-saved-sql-rename="startRenamingSavedSqlNode"
|
||||
@open-danger-dialog="openSidebarDangerDialog"
|
||||
@open-dialog-controller="updateSidebarTreeItemDialogController"
|
||||
@open-install-extension="openSidebarInstallExtension"
|
||||
@@ -1933,11 +1986,11 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
:node="item.node"
|
||||
:depth="item.depth"
|
||||
:reorder-disabled="isRootListPartial || isConnectionListAlphabeticallySorted"
|
||||
:pending-rename="pendingRenameGroupId === item.node.id"
|
||||
:pending-rename="pendingRenameNodeId === item.node.id"
|
||||
:highlighted="highlightedNodeId === item.node.id"
|
||||
:comment-label-width="sidebarCommentLabelWidths.get(item.node.id)"
|
||||
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
|
||||
@rename-started="pendingRenameGroupId = null"
|
||||
@rename-started="pendingRenameNodeId = null"
|
||||
@group-created="startRenamingCreatedGroup"
|
||||
/>
|
||||
</template>
|
||||
@@ -1980,11 +2033,11 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
:node="item.node"
|
||||
:depth="item.depth"
|
||||
:reorder-disabled="isRootListPartial || isConnectionListAlphabeticallySorted"
|
||||
:pending-rename="pendingRenameGroupId === item.node.id"
|
||||
:pending-rename="pendingRenameNodeId === item.node.id"
|
||||
:highlighted="highlightedNodeId === item.id"
|
||||
:comment-label-width="sidebarCommentLabelWidths.get(item.node.id)"
|
||||
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
|
||||
@rename-started="pendingRenameGroupId = null"
|
||||
@rename-started="pendingRenameNodeId = null"
|
||||
@group-created="startRenamingCreatedGroup"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stor
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
||||
import type { ColumnInfo, DatabaseType, TreeNode, TreeNodeType } from "@/types/database";
|
||||
@@ -157,6 +158,8 @@ import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntim
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { rankSavedSqlHistory, type SavedSqlHistoryScope } from "@/lib/savedSql/savedSqlHistory";
|
||||
import { savedSqlClipboardFileIds, savedSqlPasteTargetForNode } from "@/lib/savedSql/savedSqlClipboard";
|
||||
import { exportSavedSqlFileContent } from "@/lib/savedSql/savedSqlExport";
|
||||
import { isSqlServerLinkedNode } from "@/lib/database/sqlServerLinkedServers";
|
||||
import { flattenTree } from "@/composables/useFlatTree";
|
||||
import { createDatabaseCollationOptionsForCharset, nextCreateDatabaseCollation, normalizeCreateDatabaseCharset, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
@@ -333,6 +336,7 @@ const emit = defineEmits<{
|
||||
"rename-started": [];
|
||||
"group-created": [groupId: string];
|
||||
"request-group-rename": [groupId: string];
|
||||
"request-saved-sql-rename": [nodeId: string];
|
||||
"node-toggled": [node: TreeNode, expanded: boolean];
|
||||
"search-toggle": [node: TreeNode];
|
||||
"context-menu": [event: MouseEvent, node: TreeNode, items: ContextMenuItem[]];
|
||||
@@ -627,6 +631,12 @@ async function toggle() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "saved-sql-root" || node.type === "saved-sql-folder") {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "group-extensions" && connectionStore.canUseLoadedTreeNodeToggle(node)) {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
if (wasExpanded && !connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
|
||||
@@ -846,6 +856,8 @@ function runRowClickAction(clickDetail: number) {
|
||||
openObjectSourceDialog(false);
|
||||
} else if (action === "open-extension-details") {
|
||||
emit("open-extension-details", node);
|
||||
} else if (action === "open-saved-sql") {
|
||||
void openSavedSqlFile();
|
||||
} else if (isDocumentBrowserTreeNode(node.type)) {
|
||||
openMongoTreeData(node);
|
||||
} else if (action === "toggle") {
|
||||
@@ -1014,11 +1026,23 @@ function openTransferFromTreeClipboard(): boolean {
|
||||
}
|
||||
|
||||
function requestPasteTreeClipboard(): boolean {
|
||||
if (currentDatabaseType() === "victoriametrics") return false;
|
||||
claimTreeItemDialogOwnership();
|
||||
ensureDangerDialogRouting();
|
||||
routeTreeItemDialogController();
|
||||
const clipboard = connectionStore.treeClipboard;
|
||||
if (clipboard?.kind === "saved-sql-copy") {
|
||||
const target = savedSqlPasteTargetForNode(activeNode.value);
|
||||
if (!target || clipboard.fileIds.length === 0) return false;
|
||||
void savedSqlStore
|
||||
.copyFilesToDatabase(clipboard.fileIds, target)
|
||||
.then((files) => {
|
||||
if (files.length > 0) toast(t("savedSql.pasted", { count: files.length }), 2000);
|
||||
else toast(t("savedSql.nothingToPaste"), 3000);
|
||||
})
|
||||
.catch((e: unknown) => toast(t("savedSql.pasteFailed", { message: savedSqlErrorMessage(e, t) }), 5000));
|
||||
return true;
|
||||
}
|
||||
if (currentDatabaseType() === "victoriametrics") return false;
|
||||
if (clipboard?.kind === "connection-copy") {
|
||||
const targetGroupId = connectionPasteTargetGroupId(activeNode.value, (connectionId) => connectionStore.groupIdForConnection(connectionId));
|
||||
void connectionStore
|
||||
@@ -1079,6 +1103,10 @@ function requestRenameSelectedNode(): boolean {
|
||||
startRenameGroup();
|
||||
return true;
|
||||
}
|
||||
if (activeNode.value.type === "saved-sql-file" && activeNode.value.savedSqlId) {
|
||||
emit("request-saved-sql-rename", activeNode.value.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1117,6 +1145,10 @@ function requestDeleteSelectedNode(): boolean {
|
||||
claimTreeItemDialogOwnership();
|
||||
ensureDangerDialogRouting();
|
||||
routeTreeItemDialogController();
|
||||
if (activeNode.value.type === "saved-sql-file" && activeNode.value.savedSqlId) {
|
||||
showDeleteSavedSqlConfirm.value = true;
|
||||
return true;
|
||||
}
|
||||
if (requestDropSelectedNodes()) return true;
|
||||
if (activeNode.value.type === "connection") {
|
||||
deleteConnection();
|
||||
@@ -1230,6 +1262,59 @@ async function openSavedSqlFile() {
|
||||
void savedSqlStore.recordFileUsage(file.id);
|
||||
}
|
||||
|
||||
async function copySavedSqlFiles() {
|
||||
const selected = selectedTreeNodesInVisibleOrder();
|
||||
const nodes = selected.length > 1 && selected.some((node) => node.id === activeNode.value.id) ? selected : [activeNode.value];
|
||||
const fileIds = savedSqlClipboardFileIds(nodes);
|
||||
if (fileIds.length === 0) return;
|
||||
connectionStore.treeClipboard = { kind: "saved-sql-copy", fileIds };
|
||||
try {
|
||||
await copyToClipboard(
|
||||
nodes
|
||||
.filter((node) => node.type === "saved-sql-file" && !!node.savedSqlId)
|
||||
.map(copyNameForTreeNode)
|
||||
.join("\n"),
|
||||
);
|
||||
} catch {
|
||||
/* Internal SQL clipboard remains usable when the system clipboard is unavailable. */
|
||||
}
|
||||
toast(t("savedSql.copied", { count: fileIds.length }), 2000);
|
||||
}
|
||||
|
||||
async function exportSavedSqlFile() {
|
||||
const node = activeNode.value;
|
||||
if (node.type !== "saved-sql-file" || !node.savedSqlId) return;
|
||||
try {
|
||||
const file = await savedSqlStore.ensureFileContent(node.savedSqlId);
|
||||
if (!file) return;
|
||||
const result = await exportSavedSqlFileContent(file.sql, file.name);
|
||||
if (result === "saved") toast(t("sqlLibrary.exported"), 2000);
|
||||
} catch (e: any) {
|
||||
toast(t("sqlLibrary.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function renameSavedSqlFile() {
|
||||
const node = activeNode.value;
|
||||
if (node.type === "saved-sql-file" && node.savedSqlId) emit("request-saved-sql-rename", node.id);
|
||||
}
|
||||
|
||||
function deleteSavedSqlFile() {
|
||||
const node = activeNode.value;
|
||||
if (node.type !== "saved-sql-file" || !node.savedSqlId) return;
|
||||
claimTreeItemDialogOwnership();
|
||||
ensureDangerDialogRouting();
|
||||
showDeleteSavedSqlConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmDeleteSavedSqlFile() {
|
||||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
if (node.type !== "saved-sql-file" || !node.savedSqlId) return;
|
||||
await savedSqlStore.deleteFile(node.savedSqlId);
|
||||
connectionStore.removeTreeNode(node.id);
|
||||
releaseActiveNodeReference([node.id]);
|
||||
}
|
||||
|
||||
async function openObjectBrowser() {
|
||||
const node = activeNode.value;
|
||||
if (!node.connectionId) return;
|
||||
@@ -1631,6 +1716,22 @@ async function copySelectedNames() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const savedSqlFileIds = savedSqlClipboardFileIds(nodes);
|
||||
if (savedSqlFileIds.length > 0) {
|
||||
connectionStore.treeClipboard = { kind: "saved-sql-copy", fileIds: savedSqlFileIds };
|
||||
try {
|
||||
await copyToClipboard(
|
||||
nodes
|
||||
.filter((node) => node.type === "saved-sql-file" && !!node.savedSqlId)
|
||||
.map(copyNameForTreeNode)
|
||||
.join("\n"),
|
||||
);
|
||||
} catch {
|
||||
/* Internal SQL clipboard remains usable when the system clipboard is unavailable. */
|
||||
}
|
||||
toast(t("savedSql.copied", { count: savedSqlFileIds.length }), 2000);
|
||||
return;
|
||||
}
|
||||
updateTreeClipboardForNodes(nodes);
|
||||
try {
|
||||
await copyToClipboard(nodes.map(copyNameForTreeNode).join("\n"));
|
||||
@@ -3603,6 +3704,7 @@ const isMultiSelected = computed(() => connectionStore.selectedTreeNodeIdsSet.ha
|
||||
|
||||
const dangerDialogRoutes: Array<{ flag: { value: boolean }; createRequest: () => SidebarDangerDialogRequest }> = [];
|
||||
const showHBaseDeleteTableConfirm = shallowRef(false);
|
||||
const showDeleteSavedSqlConfirm = shallowRef(false);
|
||||
|
||||
let stopDangerDialogRouting: (() => void) | null = null;
|
||||
|
||||
@@ -3680,6 +3782,15 @@ routeDangerDialog(showHBaseDeleteTableConfirm, () => {
|
||||
});
|
||||
});
|
||||
|
||||
routeDangerDialog(showDeleteSavedSqlConfirm, () =>
|
||||
dangerRequest({
|
||||
title: t("savedSql.deleteFile"),
|
||||
message: t("savedSql.deleteFileConfirm", { name: activeNode.value.label }),
|
||||
confirmLabel: t("dangerDialog.confirm"),
|
||||
confirm: confirmDeleteSavedSqlFile,
|
||||
}),
|
||||
);
|
||||
|
||||
routeDangerDialog(showEmptyTableConfirm, () =>
|
||||
dangerRequest({
|
||||
title: t("contextMenu.confirmEmptyTableTitle"),
|
||||
@@ -4065,6 +4176,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
const shortcutCopyName = computed(() => settingsStore.editorSettings.shortcuts.copySidebarSelection);
|
||||
|
||||
const shortcutPaste = computed(() => settingsStore.editorSettings.shortcuts.pasteSidebarSelection);
|
||||
|
||||
const shortcutOpenDataInNewTab = computed(() => settingsStore.editorSettings.shortcuts.openDataInNewTab);
|
||||
|
||||
const shortcutEditConnection = computed(() => settingsStore.editorSettings.shortcuts.editSidebarConnection);
|
||||
@@ -4116,6 +4229,7 @@ function savedSqlHistoryScopeForNode(node: TreeNode): SavedSqlHistoryScope | nul
|
||||
if ((node.type === "database" || node.type === "schema") && hasTreeNodeDatabaseContext(node)) {
|
||||
return {
|
||||
connectionId: node.connectionId,
|
||||
catalog: node.catalog ?? null,
|
||||
database: node.database,
|
||||
schema: node.type === "schema" ? node.schema : undefined,
|
||||
};
|
||||
@@ -4123,6 +4237,7 @@ function savedSqlHistoryScopeForNode(node: TreeNode): SavedSqlHistoryScope | nul
|
||||
if ((node.type === "table" || node.type === "view") && hasTreeNodeDatabaseContext(node)) {
|
||||
return {
|
||||
connectionId: node.connectionId,
|
||||
catalog: node.catalog ?? null,
|
||||
database: node.database,
|
||||
schema: node.schema,
|
||||
tableName: node.label,
|
||||
@@ -4474,6 +4589,36 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
|
||||
function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
const { node, items } = context;
|
||||
|
||||
if (node.type === "saved-sql-root") {
|
||||
items.push({
|
||||
label: t("savedSql.pasteFile"),
|
||||
action: () => requestPasteTreeClipboard(),
|
||||
icon: Clipboard,
|
||||
shortcut: shortcutPaste.value,
|
||||
disabled: connectionStore.treeClipboard?.kind !== "saved-sql-copy" || !savedSqlPasteTargetForNode(node),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.type === "saved-sql-file") {
|
||||
items.push({ label: t("savedSql.open"), action: openSavedSqlFile, icon: FileCode });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("savedSql.copyFile"), action: copySavedSqlFiles, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({
|
||||
label: t("savedSql.pasteFile"),
|
||||
action: () => requestPasteTreeClipboard(),
|
||||
icon: Clipboard,
|
||||
shortcut: shortcutPaste.value,
|
||||
disabled: connectionStore.treeClipboard?.kind !== "saved-sql-copy" || !savedSqlPasteTargetForNode(node),
|
||||
});
|
||||
items.push({ label: t("sqlLibrary.exportFile"), action: exportSavedSqlFile, icon: Upload });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("savedSql.renameFile"), action: renameSavedSqlFile, icon: Pencil, shortcut: shortcutRename });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("savedSql.deleteFile"), action: deleteSavedSqlFile, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
|
||||
return true;
|
||||
}
|
||||
// 5. Redis DB / Mongo DB
|
||||
if (currentDatabaseType() === "hbase" && node.type === "group-tables") {
|
||||
if (canCreateTable.value) {
|
||||
|
||||
@@ -68,6 +68,9 @@ import { isWindows } from "@/lib/backend/platform";
|
||||
import { flattenTree } from "@/composables/useFlatTree";
|
||||
import { productionContextForDatabase } from "@/lib/database/productionSafety";
|
||||
import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus";
|
||||
import { ensureSqlExtension, stripSqlExtension } from "@/lib/savedSql/savedSqlFileName";
|
||||
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
// --- Drag and Drop ---
|
||||
import { useDragSort } from "@/composables/useDragSort";
|
||||
import { sidebarTreeRuntimeKey } from "@/lib/sidebar/sidebarTreeRuntime";
|
||||
@@ -261,6 +264,12 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
return { icon: ShieldCheck, colorClass: "text-primary" };
|
||||
case "dameng-job-admin":
|
||||
return { icon: CalendarClock, colorClass: "text-primary" };
|
||||
case "saved-sql-root":
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-blue-500" };
|
||||
case "saved-sql-folder":
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-blue-400" };
|
||||
case "saved-sql-file":
|
||||
return { icon: FileCode, colorClass: "text-blue-400" };
|
||||
case "index":
|
||||
return { icon: Key, colorClass: "text-amber-400" };
|
||||
case "fkey":
|
||||
@@ -355,6 +364,7 @@ function displayLabel(node: TreeNode): string {
|
||||
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
|
||||
if (node.type === "user-admin" || node.type === "dameng-users" || node.type === "dameng-roles" || node.type === "dameng-job-admin") return t(node.label);
|
||||
if (node.type === "linked-server-root") return t(node.label);
|
||||
if (node.type === "saved-sql-root") return t(node.label);
|
||||
if (node.type === "mqtt-topic" && node.id.endsWith(":mqtt-topic:__console__")) return t(node.label);
|
||||
if (node.label === "tree.defaultDatabase") return t(node.label);
|
||||
return isGroupLabel(node) ? t(node.label) : node.label;
|
||||
@@ -828,6 +838,8 @@ function clearTableSearchQuery() {
|
||||
// --- Connection Group Management ---
|
||||
const isRenamingGroup = ref(false);
|
||||
|
||||
const isRenamingSavedSql = ref(false);
|
||||
|
||||
const renameInput = ref("");
|
||||
|
||||
const renameInputRef = ref<HTMLInputElement>();
|
||||
@@ -839,10 +851,20 @@ function startRenameGroup() {
|
||||
focusSidebarRenameInput(() => (isRenamingGroup.value ? renameInputRef.value : undefined));
|
||||
}
|
||||
|
||||
function startRenameSavedSql() {
|
||||
if (activeNode.value.type !== "saved-sql-file" || !activeNode.value.savedSqlId) return;
|
||||
renameInput.value = stripSqlExtension(activeNode.value.label);
|
||||
isRenamingSavedSql.value = true;
|
||||
emit("rename-started");
|
||||
focusSidebarRenameInput(() => (isRenamingSavedSql.value ? renameInputRef.value : undefined));
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.pendingRename,
|
||||
(pending) => {
|
||||
if (pending && activeNode.value.type === "connection-group") startRenameGroup();
|
||||
if (!pending) return;
|
||||
if (activeNode.value.type === "connection-group") startRenameGroup();
|
||||
else if (activeNode.value.type === "saved-sql-file") startRenameSavedSql();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -850,7 +872,7 @@ watch(
|
||||
function shouldMeasureLabelOverflow(): boolean {
|
||||
return shouldMeasureSidebarLabelOverflow({
|
||||
hasDetailTooltip: !!detailTooltip.value?.rows.length,
|
||||
isRenaming: isRenamingGroup.value,
|
||||
isRenaming: isRenamingGroup.value || isRenamingSavedSql.value,
|
||||
usesFullWidthLabel: usesFullWidthLabel.value,
|
||||
});
|
||||
}
|
||||
@@ -870,6 +892,30 @@ function finishRenameGroup() {
|
||||
connectionStore.renameConnectionGroup(activeNode.value.id, trimmed);
|
||||
}
|
||||
|
||||
async function finishRenameSavedSql() {
|
||||
if (!isRenamingSavedSql.value) return;
|
||||
isRenamingSavedSql.value = false;
|
||||
const fileId = activeNode.value.savedSqlId;
|
||||
const trimmed = renameInput.value.trim();
|
||||
if (!fileId || !trimmed || ensureSqlExtension(trimmed) === activeNode.value.label) return;
|
||||
try {
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
await savedSqlStore.renameFile(fileId, ensureSqlExtension(trimmed));
|
||||
} catch (e: any) {
|
||||
toast(t("savedSql.renameFailed", { message: savedSqlErrorMessage(e, t) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function finishRename() {
|
||||
if (isRenamingSavedSql.value) void finishRenameSavedSql();
|
||||
else finishRenameGroup();
|
||||
}
|
||||
|
||||
function cancelRename() {
|
||||
isRenamingGroup.value = false;
|
||||
isRenamingSavedSql.value = false;
|
||||
}
|
||||
|
||||
const PINNED_TREE_NODE_DRAG_TYPE = "__pinned-tree-node__";
|
||||
|
||||
function pinnedSortKey(): string {
|
||||
@@ -1081,6 +1127,7 @@ watch(
|
||||
// Virtual rows are recycled; transient DOM and pointer state must not leak
|
||||
// from the previously rendered node into the new row.
|
||||
isRenamingGroup.value = false;
|
||||
isRenamingSavedSql.value = false;
|
||||
renameInput.value = "";
|
||||
labelOverflowing.value = false;
|
||||
suppressNextTableReferenceClick = false;
|
||||
@@ -1244,13 +1291,13 @@ function onKeydown(event: KeyboardEvent) {
|
||||
<div ref="trailingCommentLayoutRef" :class="hasTrailingMetadata() ? 'flex flex-1 min-w-0 items-center' : 'contents'">
|
||||
<div ref="trailingCommentLeadingRef" :class="trailingComment ? 'flex max-w-full min-w-0 shrink-0 items-center gap-2' : formattedObjectStorage() ? 'flex min-w-0 flex-1 items-center gap-2' : 'contents'" :style="alignedCommentLeadingStyle()">
|
||||
<input
|
||||
v-if="isRenamingGroup"
|
||||
v-if="isRenamingGroup || isRenamingSavedSql"
|
||||
ref="renameInputRef"
|
||||
v-model="renameInput"
|
||||
class="min-w-0 flex-1 truncate bg-transparent border border-primary/50 rounded px-1 outline-none"
|
||||
@blur="finishRenameGroup"
|
||||
@keydown.enter.prevent="finishRenameGroup"
|
||||
@keydown.escape.prevent="isRenamingGroup = false"
|
||||
@blur="finishRename"
|
||||
@keydown.enter.prevent="finishRename"
|
||||
@keydown.escape.prevent="cancelRename"
|
||||
@click.stop
|
||||
/>
|
||||
<span v-else ref="labelRef" :class="[labelWidthClass, { 'flex-1': node.type === 'connection' && !trailingComment }]">{{ visibleLabel(node) }}</span>
|
||||
|
||||
Reference in new issue
Block a user