fix(sidebar): centralize tree action handling
This commit is contained in:
1 parent
7f096653db
commit
ee85033068
28 files changed
+2705
-1032
No files matched your search
@@ -6,10 +6,10 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { TreeNode, TreeNodeType } from "@/types/database";
|
||||
import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import { filterSidebarSearchRootsByConnectionState, filterSidebarTree } from "@/lib/sidebar/sidebarSearchTree";
|
||||
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { copyNameForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { copyNameForTreeNode, objectSourceKindForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
|
||||
@@ -17,14 +17,24 @@ import { usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import { 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";
|
||||
import { SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import { createFlatTreeIndex, SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import { createSidebarPasteHandlerRegistry } from "@/lib/sidebar/sidebarPasteHandlerRegistry";
|
||||
import { insertSidebarTableSearchControls, isSidebarTableSearchControlNode } from "@/lib/sidebar/sidebarTableSearchControl";
|
||||
import TreeItem from "./TreeItem.vue";
|
||||
import SidebarTreeItemDialogs from "./SidebarTreeItemDialogs.vue";
|
||||
import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog.vue";
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||
import LightDropdown from "@/components/ui/LightDropdown.vue";
|
||||
import { cancelPendingSidebarDataOpen, runSidebarDataOpenImmediately, type SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import { codeMirrorSqlDialect } from "@/lib/database/jdbcDialect";
|
||||
import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
|
||||
import { createSidebarActionTarget, findSidebarActionTarget, type SidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget";
|
||||
import type { SidebarDangerDialogRequest } from "@/lib/sidebar/sidebarDangerDialog";
|
||||
import { resetSidebarTreeDialogState } from "./sidebarTreeDialogState";
|
||||
import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs";
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
@@ -39,6 +49,37 @@ const pointerInsideTree = ref(false);
|
||||
const treeScrollerRef = ref<InstanceType<typeof RecycleScroller> | null>(null);
|
||||
const plainTreeScrollerRef = ref<HTMLElement | null>(null);
|
||||
const sidebarScrollbarTrackRef = ref<HTMLElement | null>(null);
|
||||
const sidebarContextMenuRef = ref<{ close: () => void } | null>(null);
|
||||
const sidebarContextMenuItems = ref<ContextMenuItem[]>([]);
|
||||
const sidebarContextMenuTarget = ref<SidebarActionTarget | null>(null);
|
||||
const sidebarDangerDialogRequest = ref<SidebarDangerDialogRequest | null>(null);
|
||||
const sidebarDangerDialogOpen = ref(false);
|
||||
const sidebarDangerDialogConfirming = ref(false);
|
||||
const sidebarTreeItemDialogController = ref<Record<string, any> | null>(null);
|
||||
const sidebarInstallExtensionTarget = ref<TreeNode | null>(null);
|
||||
const sidebarInstallExtensionDialogRef = ref<InstanceType<typeof InstallExtensionDialog> | null>(null);
|
||||
const sidebarDdlTarget = ref<TreeNode | null>(null);
|
||||
const sidebarDdlOpen = ref(false);
|
||||
const sidebarObjectSourceTarget = ref<{ node: TreeNode; initialEditing: boolean } | null>(null);
|
||||
const sidebarObjectSourceOpen = ref(false);
|
||||
const sidebarProcedureTarget = ref<TreeNode | null>(null);
|
||||
const sidebarProcedureOpen = ref(false);
|
||||
const sidebarVisibleDatabasesTarget = ref<TreeNode | null>(null);
|
||||
const sidebarVisibleDatabasesOpen = ref(false);
|
||||
const sidebarVisibleSchemasTarget = ref<TreeNode | null>(null);
|
||||
const sidebarVisibleSchemasOpen = ref(false);
|
||||
let sidebarActionGeneration = 0;
|
||||
const sidebarDdlDatabaseType = computed(() => {
|
||||
const connectionId = sidebarDdlTarget.value?.connectionId;
|
||||
return connectionId ? effectiveDatabaseTypeForConnection(store.getConfig(connectionId)) : undefined;
|
||||
});
|
||||
const sidebarObjectSourceType = computed(() => (sidebarObjectSourceTarget.value ? objectSourceKindForTreeNode(sidebarObjectSourceTarget.value.node.type) : null));
|
||||
const sidebarObjectSourceDatabaseType = computed(() => {
|
||||
const connectionId = sidebarObjectSourceTarget.value?.node.connectionId;
|
||||
return connectionId ? effectiveDatabaseTypeForConnection(store.getConfig(connectionId)) : undefined;
|
||||
});
|
||||
const sidebarObjectSourceDialect = computed(() => codeMirrorSqlDialect(sidebarObjectSourceDatabaseType.value));
|
||||
const sidebarObjectSourceFormatDialect = computed(() => sqlFormatDialectForDbType(sidebarObjectSourceDatabaseType.value));
|
||||
type SearchScope = "connection" | "database" | "schema" | "table" | "view";
|
||||
const selectedSearchScopes = ref<SearchScope[]>([]);
|
||||
const searchCollapsedIds = ref<Set<string>>(new Set());
|
||||
@@ -288,13 +329,19 @@ const flatNodes = computed<FlatTreeNode[]>(() =>
|
||||
activeQueries: store.sidebarTableSearchQueries,
|
||||
}),
|
||||
);
|
||||
const visibleNodes = computed<TreeNode[]>(() => flatNodes.value.map((item) => item.node));
|
||||
const selectableVisibleNodes = computed<TreeNode[]>(() => visibleNodes.value.filter((node) => !isSidebarTableSearchControlNode(node)));
|
||||
const selectableVisibleNodeIndexById = computed(() => {
|
||||
const next = new Map<string, number>();
|
||||
selectableVisibleNodes.value.forEach((node, index) => next.set(node.id, index));
|
||||
return next;
|
||||
});
|
||||
// Build all lookup tables in one linear pass whenever the visible tree changes.
|
||||
// Selection, scrolling and sticky headers then avoid repeated full-array scans.
|
||||
const flatTreeIndex = computed(() =>
|
||||
createFlatTreeIndex(flatNodes.value, {
|
||||
isSelectable: (node) => !isSidebarTableSearchControlNode(node),
|
||||
isBoundary: (type) => type === "connection" || type === "connection-group",
|
||||
isDatabaseContainer: (type) => DATABASE_LEVEL_TYPES.has(type),
|
||||
isSchemaContainer: (type) => SCHEMA_LEVEL_TYPES.has(type),
|
||||
}),
|
||||
);
|
||||
const visibleNodes = computed<TreeNode[]>(() => flatTreeIndex.value.visibleNodes);
|
||||
const selectableVisibleNodes = computed<TreeNode[]>(() => flatTreeIndex.value.selectableVisibleNodes);
|
||||
const selectableVisibleNodeIndexById = computed(() => flatTreeIndex.value.selectableVisibleNodeIndexById);
|
||||
const useVirtualTree = computed(() => shouldVirtualizeFlatTree(flatNodes.value.length));
|
||||
const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId));
|
||||
|
||||
@@ -382,51 +429,19 @@ const stickyNode = computed<FlatTreeNode | null>(() => {
|
||||
if (len === 0) return null;
|
||||
|
||||
const topIndex = Math.min(Math.floor(stickyScrollTop.value / SIDEBAR_TREE_ROW_HEIGHT), len - 1);
|
||||
// flatNodes is a DFS preorder spanning ALL connections, so walking up from a
|
||||
// leaf visits `... -> schema -> database -> connection -> <other connection>`.
|
||||
// Stop at the connection boundary so the sticky row never leaks across into a
|
||||
// different connection's subtree (e.g. MySQL's last database sticking while
|
||||
// scrolling Dameng). Within one connection: track both candidates and prefer
|
||||
// database-level; only fall back to schema when the whole path has no
|
||||
// database-level container (Dameng/Oracle-style trees).
|
||||
let schemaCandidate: FlatTreeNode | null = null;
|
||||
let schemaCandidateTop = 0;
|
||||
for (let i = topIndex; i >= 0; i--) {
|
||||
const item = nodes[i];
|
||||
if (item.type === "connection" || item.type === "connection-group") break;
|
||||
if (DATABASE_LEVEL_TYPES.has(item.type)) {
|
||||
const rowTop = i * SIDEBAR_TREE_ROW_HEIGHT;
|
||||
return stickyScrollTop.value > rowTop ? item : null;
|
||||
}
|
||||
if (item.type === "schema" && !schemaCandidate) {
|
||||
schemaCandidate = item;
|
||||
schemaCandidateTop = i * SIDEBAR_TREE_ROW_HEIGHT;
|
||||
}
|
||||
}
|
||||
if (!schemaCandidate) return null;
|
||||
return stickyScrollTop.value > schemaCandidateTop ? schemaCandidate : null;
|
||||
const containerIndex = flatTreeIndex.value.stickyContainerIndexByIndex[topIndex] ?? -1;
|
||||
if (containerIndex < 0) return null;
|
||||
return stickyScrollTop.value > containerIndex * SIDEBAR_TREE_ROW_HEIGHT ? nodes[containerIndex] : null;
|
||||
});
|
||||
|
||||
const stickyHeaderStyle = computed<CSSProperties>(() => {
|
||||
const node = stickyNode.value;
|
||||
if (!node) return {};
|
||||
const nodes = flatNodes.value;
|
||||
const currentIndex = nodes.findIndex((item) => item.id === node.id);
|
||||
const currentIndex = flatTreeIndex.value.flatNodeIndexById.get(node.id) ?? -1;
|
||||
if (currentIndex < 0) return {};
|
||||
// Look forward for the next sibling container at the SAME level as the sticky
|
||||
// node so the push-up only fires when a peer scrolls in (database-to-database,
|
||||
// or schema-to-schema for Dameng/Oracle), never schema-into-database. Stop at
|
||||
// the connection boundary so we never reach into the next connection's rows.
|
||||
const nextTypes = SCHEMA_LEVEL_TYPES.has(node.type) ? SCHEMA_LEVEL_TYPES : DATABASE_LEVEL_TYPES;
|
||||
let nextDatabaseIndex = -1;
|
||||
for (let i = currentIndex + 1; i < nodes.length; i++) {
|
||||
const item = nodes[i];
|
||||
if (item.type === "connection" || item.type === "connection-group") break;
|
||||
if (nextTypes.has(item.type)) {
|
||||
nextDatabaseIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The next peer index is precomputed with the flat-tree snapshot so scrolling
|
||||
// never scans the remaining tree. Connection boundaries reset the lookup.
|
||||
const nextDatabaseIndex = SCHEMA_LEVEL_TYPES.has(node.type) ? flatTreeIndex.value.nextSchemaContainerIndexByIndex[currentIndex] : flatTreeIndex.value.nextDatabaseContainerIndexByIndex[currentIndex];
|
||||
if (nextDatabaseIndex < 0) return {};
|
||||
const distanceToNext = nextDatabaseIndex * SIDEBAR_TREE_ROW_HEIGHT - stickyScrollTop.value;
|
||||
if (distanceToNext >= SIDEBAR_TREE_ROW_HEIGHT) return {};
|
||||
@@ -438,6 +453,11 @@ const stickyHeaderStyle = computed<CSSProperties>(() => {
|
||||
// Reset tracking when the tree rebuilds (connect/disconnect/collapse) so a
|
||||
// stale scrollTop doesn't keep the overlay mounted after a structural change.
|
||||
watch(flatNodes, () => {
|
||||
// Menu actions originate from a rendered row instance. Close the singleton
|
||||
// before a structural update can recycle that row onto another node.
|
||||
sidebarContextMenuRef.value?.close();
|
||||
sidebarContextMenuItems.value = [];
|
||||
sidebarContextMenuTarget.value = null;
|
||||
stickyScrollTop.value = 0;
|
||||
void nextTick(scheduleSidebarScrollMetricsUpdate);
|
||||
});
|
||||
@@ -567,7 +587,7 @@ function topOcclusionHeightForSidebarNode(nodeId: string): number {
|
||||
async function scrollToSidebarNode(nodeId: string, options?: { align?: SidebarNodeScrollAlign }) {
|
||||
await nextTick();
|
||||
|
||||
const index = flatNodes.value.findIndex((item) => item.id === nodeId);
|
||||
const index = flatTreeIndex.value.flatNodeIndexById.get(nodeId) ?? -1;
|
||||
const scroller = currentTreeScroller();
|
||||
if (!scroller || index < 0) return;
|
||||
|
||||
@@ -845,10 +865,177 @@ function onSearchToggle(node: TreeNode) {
|
||||
searchCollapsedIds.value = next;
|
||||
}
|
||||
|
||||
function openSidebarContextMenu(event: MouseEvent, node: TreeNode, items: ContextMenuItem[], openContextMenu: (event: MouseEvent, itemsOverride?: ContextMenuItem[]) => void) {
|
||||
sidebarContextMenuTarget.value = createSidebarActionTarget(node);
|
||||
sidebarContextMenuItems.value = items;
|
||||
// Pass the current row's resolved menu atomically. Waiting for the items prop
|
||||
// to flush would let the singleton menu briefly reuse the previous row menu.
|
||||
openContextMenu(event, items);
|
||||
}
|
||||
|
||||
function openSidebarDangerDialog(request: SidebarDangerDialogRequest) {
|
||||
sidebarDangerDialogRequest.value = request;
|
||||
sidebarDangerDialogConfirming.value = false;
|
||||
sidebarDangerDialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function confirmSidebarDangerDialog() {
|
||||
const request = sidebarDangerDialogRequest.value;
|
||||
if (!request || sidebarDangerDialogConfirming.value) return;
|
||||
if (request.closeOnConfirm !== false) sidebarDangerDialogOpen.value = false;
|
||||
sidebarDangerDialogConfirming.value = true;
|
||||
try {
|
||||
await request.confirm();
|
||||
sidebarDangerDialogOpen.value = false;
|
||||
} finally {
|
||||
sidebarDangerDialogConfirming.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSidebarDangerDialogOption(event: Event) {
|
||||
const option = sidebarDangerDialogRequest.value?.option;
|
||||
if (!option) return;
|
||||
option.checked = (event.target as HTMLInputElement).checked;
|
||||
void option.onChange?.(option.checked);
|
||||
}
|
||||
|
||||
function updateSidebarTreeItemDialogController(controller: Record<string, any> | null) {
|
||||
sidebarTreeItemDialogController.value = controller;
|
||||
}
|
||||
|
||||
async function openSidebarInstallExtension(node: TreeNode) {
|
||||
sidebarInstallExtensionTarget.value = createSidebarActionTarget(node);
|
||||
await nextTick();
|
||||
sidebarInstallExtensionDialogRef.value?.show();
|
||||
}
|
||||
|
||||
function beginSidebarAction(): number {
|
||||
sidebarActionGeneration += 1;
|
||||
sidebarDdlOpen.value = false;
|
||||
sidebarObjectSourceOpen.value = false;
|
||||
sidebarProcedureOpen.value = false;
|
||||
sidebarVisibleDatabasesOpen.value = false;
|
||||
sidebarVisibleSchemasOpen.value = false;
|
||||
sidebarDdlTarget.value = null;
|
||||
sidebarObjectSourceTarget.value = null;
|
||||
sidebarProcedureTarget.value = null;
|
||||
sidebarVisibleDatabasesTarget.value = null;
|
||||
sidebarVisibleSchemasTarget.value = null;
|
||||
return sidebarActionGeneration;
|
||||
}
|
||||
|
||||
function tableDdlObjectTypeForSidebarNode(type: TreeNodeType): ObjectSourceKind | undefined {
|
||||
if (type === "view") return "VIEW";
|
||||
if (type === "materialized_view") return "MATERIALIZED_VIEW";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function openSidebarDdl(node: TreeNode) {
|
||||
if (!node.connectionId || !node.database) return;
|
||||
beginSidebarAction();
|
||||
sidebarDdlTarget.value = createSidebarActionTarget(node);
|
||||
sidebarDdlOpen.value = true;
|
||||
}
|
||||
|
||||
function openSidebarObjectSource(node: TreeNode, initialEditing: boolean) {
|
||||
if (!node.connectionId || !node.database || !objectSourceKindForTreeNode(node.type)) return;
|
||||
const target = createSidebarActionTarget(node);
|
||||
const requestGeneration = beginSidebarAction();
|
||||
void store
|
||||
.ensureConnected(target.connectionId!)
|
||||
.then(() => {
|
||||
if (requestGeneration !== sidebarActionGeneration) return;
|
||||
store.activeConnectionId = target.connectionId!;
|
||||
sidebarObjectSourceTarget.value = { node: target, initialEditing };
|
||||
sidebarObjectSourceOpen.value = true;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
if (requestGeneration === sidebarActionGeneration) toast(error?.message || String(error), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function openSidebarProcedure(node: TreeNode) {
|
||||
if (node.type !== "procedure" || !node.connectionId || !node.database) return;
|
||||
beginSidebarAction();
|
||||
sidebarProcedureTarget.value = createSidebarActionTarget(node);
|
||||
sidebarProcedureOpen.value = true;
|
||||
}
|
||||
|
||||
function openSidebarData(node: TreeNode, requireSelection: boolean, runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise<void>) {
|
||||
const target = createSidebarActionTarget(node);
|
||||
runSidebarDataOpenImmediately((request) => {
|
||||
if (requireSelection && store.selectedTreeNodeId !== target.id) return;
|
||||
return runner(target, request);
|
||||
});
|
||||
}
|
||||
|
||||
function openSidebarVisibleDatabases(node: TreeNode) {
|
||||
if (node.type !== "connection" || !node.connectionId) return;
|
||||
beginSidebarAction();
|
||||
sidebarVisibleDatabasesTarget.value = createSidebarActionTarget(node);
|
||||
sidebarVisibleDatabasesOpen.value = true;
|
||||
}
|
||||
|
||||
function openSidebarVisibleSchemas(node: TreeNode) {
|
||||
if ((node.type !== "connection" && node.type !== "database") || !node.connectionId) return;
|
||||
const database = node.type === "database" ? node.database : store.getConfig(node.connectionId)?.database;
|
||||
if (database == null) return;
|
||||
beginSidebarAction();
|
||||
sidebarVisibleSchemasTarget.value = createSidebarActionTarget({ ...node, database });
|
||||
sidebarVisibleSchemasOpen.value = true;
|
||||
}
|
||||
|
||||
function openSidebarProcedureSql(sql: string) {
|
||||
const target = sidebarProcedureTarget.value;
|
||||
if (!target?.connectionId || !target.database || !sql) return;
|
||||
const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema);
|
||||
queryStore.updateSql(tabId, sql);
|
||||
}
|
||||
|
||||
async function executeSidebarProcedureSql(sql: string) {
|
||||
const target = sidebarProcedureTarget.value;
|
||||
if (!target?.connectionId || !target.database || !sql) return;
|
||||
const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema);
|
||||
queryStore.updateSql(tabId, sql);
|
||||
await queryStore.executeTabSql(tabId, sql);
|
||||
}
|
||||
|
||||
async function refreshSidebarActionTarget() {
|
||||
const target = sidebarObjectSourceTarget.value?.node || sidebarDdlTarget.value;
|
||||
if (!target) return;
|
||||
const currentTarget = findSidebarActionTarget(store.treeNodes, target);
|
||||
if (!currentTarget) return;
|
||||
try {
|
||||
await store.refreshTreeNode(currentTarget);
|
||||
} catch (error: any) {
|
||||
toast(error?.message || String(error), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
watch(sidebarDdlOpen, (open) => {
|
||||
if (!open) sidebarDdlTarget.value = null;
|
||||
});
|
||||
|
||||
watch(sidebarObjectSourceOpen, (open) => {
|
||||
if (!open) sidebarObjectSourceTarget.value = null;
|
||||
});
|
||||
|
||||
watch(sidebarProcedureOpen, (open) => {
|
||||
if (!open) sidebarProcedureTarget.value = null;
|
||||
});
|
||||
|
||||
watch(sidebarVisibleDatabasesOpen, (open) => {
|
||||
if (!open) sidebarVisibleDatabasesTarget.value = null;
|
||||
});
|
||||
|
||||
watch(sidebarVisibleSchemasOpen, (open) => {
|
||||
if (!open) sidebarVisibleSchemasTarget.value = null;
|
||||
});
|
||||
|
||||
function collapseAllTreeNodes() {
|
||||
store.collapseAllTreeNodes();
|
||||
if (isSearching.value) {
|
||||
searchCollapsedIds.value = new Set(flatNodes.value.filter((item) => item.node.children?.length).map((item) => item.id));
|
||||
searchCollapsedIds.value = new Set(flatTreeIndex.value.expandableNodeIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,7 +1053,7 @@ async function selectActiveTabSidebarNode(options: { scroll: boolean }) {
|
||||
|
||||
await nextTick();
|
||||
|
||||
const index = flatNodes.value.findIndex((item) => item.id === match.id);
|
||||
const index = flatTreeIndex.value.flatNodeIndexById.get(match.id) ?? -1;
|
||||
const scroller = currentTreeScroller();
|
||||
if (!scroller || index < 0) return;
|
||||
|
||||
@@ -985,7 +1172,7 @@ function isEditConnectionShortcut(event: KeyboardEvent): boolean {
|
||||
|
||||
function requestSelectedConnectionEdit(): boolean {
|
||||
const selectedNodeId = store.selectedTreeNodeId;
|
||||
const currentNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null;
|
||||
const currentNode = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null;
|
||||
if (!currentNode) return false;
|
||||
const editTarget = selectedConnectionEditTarget(currentNode, selectedSidebarNodesInVisibleOrder());
|
||||
if (!editTarget) return false;
|
||||
@@ -1025,7 +1212,7 @@ function requestSelectedSidebarPaste(): boolean {
|
||||
const clipboard = store.treeClipboard;
|
||||
const selectedNodeId = store.selectedTreeNodeId;
|
||||
if (clipboard?.kind === "connection-copy") {
|
||||
const selectedNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null;
|
||||
const selectedNode = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null;
|
||||
const targetGroupId = connectionPasteTargetGroupId(selectedNode, (connectionId) => store.groupIdForConnection(connectionId));
|
||||
void store
|
||||
.pasteConnectionClipboard(targetGroupId)
|
||||
@@ -1045,7 +1232,19 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
sidebarActionGeneration += 1;
|
||||
sidebarContextMenuTarget.value = null;
|
||||
sidebarContextMenuItems.value = [];
|
||||
sidebarDdlTarget.value = null;
|
||||
sidebarObjectSourceTarget.value = null;
|
||||
sidebarProcedureTarget.value = null;
|
||||
sidebarVisibleDatabasesTarget.value = null;
|
||||
sidebarVisibleSchemasTarget.value = null;
|
||||
sidebarTreeItemDialogController.value = null;
|
||||
sidebarDangerDialogRequest.value = null;
|
||||
resetSidebarTreeDialogState();
|
||||
window.removeEventListener("keydown", onWindowKeydown);
|
||||
cancelPendingSidebarDataOpen();
|
||||
for (const timer of tableSearchTimers.values()) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
@@ -1107,60 +1306,172 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="flatNodes.length > 0 && useVirtualTree" class="connection-tree-scroll-shell relative min-h-0 flex-1">
|
||||
<RecycleScroller
|
||||
ref="treeScrollerRef"
|
||||
class="sidebar-tree connection-tree-scroller h-full overflow-y-auto"
|
||||
:class="sidebarTreeOverflowClass"
|
||||
@click="clearSidebarSelection"
|
||||
:items="flatNodes"
|
||||
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
|
||||
:buffer="SIDEBAR_TREE_SCROLL_BUFFER"
|
||||
:prerender="SIDEBAR_TREE_PRERENDER_COUNT"
|
||||
:skip-hover="true"
|
||||
key-field="id"
|
||||
type-field="poolType"
|
||||
flow-mode
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<CustomContextMenu ref="sidebarContextMenuRef" :items="sidebarContextMenuItems" v-slot="contextMenuSlot">
|
||||
<div v-if="flatNodes.length > 0 && useVirtualTree" class="connection-tree-scroll-shell relative min-h-0 flex-1">
|
||||
<RecycleScroller
|
||||
ref="treeScrollerRef"
|
||||
class="sidebar-tree connection-tree-scroller h-full overflow-y-auto"
|
||||
:class="sidebarTreeOverflowClass"
|
||||
@click="clearSidebarSelection"
|
||||
:items="flatNodes"
|
||||
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
|
||||
:buffer="SIDEBAR_TREE_SCROLL_BUFFER"
|
||||
:prerender="SIDEBAR_TREE_PRERENDER_COUNT"
|
||||
:skip-hover="true"
|
||||
key-field="id"
|
||||
type-field="poolType"
|
||||
flow-mode
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<TreeItem
|
||||
:node="item.node"
|
||||
:depth="item.depth"
|
||||
:drag-disabled="isFiltering"
|
||||
:pending-rename="pendingRenameGroupId === item.node.id"
|
||||
:highlighted="highlightedNodeId === item.node.id"
|
||||
@search-toggle="onSearchToggle"
|
||||
@context-menu="(event, node, items) => openSidebarContextMenu(event, node, items, contextMenuSlot.onContextMenu)"
|
||||
@open-ddl="openSidebarDdl"
|
||||
@open-object-source="openSidebarObjectSource"
|
||||
@open-procedure="openSidebarProcedure"
|
||||
@open-data="openSidebarData"
|
||||
@open-visible-databases="openSidebarVisibleDatabases"
|
||||
@open-visible-schemas="openSidebarVisibleSchemas"
|
||||
@open-danger-dialog="openSidebarDangerDialog"
|
||||
@open-dialog-controller="updateSidebarTreeItemDialogController"
|
||||
@open-install-extension="openSidebarInstallExtension"
|
||||
@rename-started="pendingRenameGroupId = null"
|
||||
@group-created="startRenamingCreatedGroup"
|
||||
/>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
<div v-if="stickyNode" class="sticky-database-header pointer-events-auto absolute inset-x-0 top-0 z-[5] border-b border-border/60" :style="stickyHeaderStyle">
|
||||
<TreeItem
|
||||
:node="stickyNode.node"
|
||||
:depth="stickyNode.depth"
|
||||
:drag-disabled="true"
|
||||
@search-toggle="onSearchToggle"
|
||||
@context-menu="(event, node, items) => openSidebarContextMenu(event, node, items, contextMenuSlot.onContextMenu)"
|
||||
@open-ddl="openSidebarDdl"
|
||||
@open-object-source="openSidebarObjectSource"
|
||||
@open-procedure="openSidebarProcedure"
|
||||
@open-data="openSidebarData"
|
||||
@open-visible-databases="openSidebarVisibleDatabases"
|
||||
@open-visible-schemas="openSidebarVisibleSchemas"
|
||||
@open-danger-dialog="openSidebarDangerDialog"
|
||||
@open-dialog-controller="updateSidebarTreeItemDialogController"
|
||||
@open-install-extension="openSidebarInstallExtension"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
|
||||
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="flatNodes.length > 0" class="connection-tree-scroll-shell relative min-h-0 flex-1">
|
||||
<div ref="plainTreeScrollerRef" class="sidebar-tree connection-tree-scroller h-full overflow-y-auto" :class="sidebarTreeOverflowClass" @click="clearSidebarSelection" @scroll.passive="onTreeScroll">
|
||||
<TreeItem
|
||||
v-for="item in flatNodes"
|
||||
:key="item.id"
|
||||
:node="item.node"
|
||||
:depth="item.depth"
|
||||
:drag-disabled="isFiltering"
|
||||
:pending-rename="pendingRenameGroupId === item.node.id"
|
||||
:highlighted="highlightedNodeId === item.node.id"
|
||||
:highlighted="highlightedNodeId === item.id"
|
||||
@search-toggle="onSearchToggle"
|
||||
@context-menu="(event, node, items) => openSidebarContextMenu(event, node, items, contextMenuSlot.onContextMenu)"
|
||||
@open-ddl="openSidebarDdl"
|
||||
@open-object-source="openSidebarObjectSource"
|
||||
@open-procedure="openSidebarProcedure"
|
||||
@open-data="openSidebarData"
|
||||
@open-visible-databases="openSidebarVisibleDatabases"
|
||||
@open-visible-schemas="openSidebarVisibleSchemas"
|
||||
@open-danger-dialog="openSidebarDangerDialog"
|
||||
@open-dialog-controller="updateSidebarTreeItemDialogController"
|
||||
@open-install-extension="openSidebarInstallExtension"
|
||||
@rename-started="pendingRenameGroupId = null"
|
||||
@group-created="startRenamingCreatedGroup"
|
||||
/>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
<div v-if="stickyNode" class="sticky-database-header pointer-events-auto absolute inset-x-0 top-0 z-[5] border-b border-border/60" :style="stickyHeaderStyle">
|
||||
<TreeItem :node="stickyNode.node" :depth="stickyNode.depth" :drag-disabled="true" @search-toggle="onSearchToggle" />
|
||||
</div>
|
||||
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
|
||||
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
|
||||
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="flatNodes.length > 0" class="connection-tree-scroll-shell relative min-h-0 flex-1">
|
||||
<div ref="plainTreeScrollerRef" class="sidebar-tree connection-tree-scroller h-full overflow-y-auto" :class="sidebarTreeOverflowClass" @click="clearSidebarSelection" @scroll.passive="onTreeScroll">
|
||||
<TreeItem
|
||||
v-for="item in flatNodes"
|
||||
:key="item.id"
|
||||
:node="item.node"
|
||||
:depth="item.depth"
|
||||
:drag-disabled="isFiltering"
|
||||
:pending-rename="pendingRenameGroupId === item.node.id"
|
||||
:highlighted="highlightedNodeId === item.id"
|
||||
@search-toggle="onSearchToggle"
|
||||
@rename-started="pendingRenameGroupId = null"
|
||||
@group-created="startRenamingCreatedGroup"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
|
||||
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
|
||||
</div>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
<SidebarDdlViewDialog
|
||||
v-if="sidebarDdlTarget"
|
||||
v-model:open="sidebarDdlOpen"
|
||||
:connection-id="sidebarDdlTarget.connectionId!"
|
||||
:database="sidebarDdlTarget.database!"
|
||||
:schema="sidebarDdlTarget.schema"
|
||||
:table-name="sidebarDdlTarget.label"
|
||||
:object-type="tableDdlObjectTypeForSidebarNode(sidebarDdlTarget.type)"
|
||||
:database-type="sidebarDdlDatabaseType"
|
||||
:dialect="codeMirrorSqlDialect(sidebarDdlDatabaseType)"
|
||||
:format-dialect="sqlFormatDialectForDbType(sidebarDdlDatabaseType)"
|
||||
/>
|
||||
|
||||
<SidebarObjectSourceDialog
|
||||
v-if="sidebarObjectSourceTarget && sidebarObjectSourceType"
|
||||
v-model:open="sidebarObjectSourceOpen"
|
||||
:connection-id="sidebarObjectSourceTarget.node.connectionId!"
|
||||
:database="sidebarObjectSourceTarget.node.database!"
|
||||
:schema="sidebarObjectSourceTarget.node.schema"
|
||||
:name="sidebarObjectSourceTarget.node.objectName || sidebarObjectSourceTarget.node.label"
|
||||
:signature="sidebarObjectSourceTarget.node.signature"
|
||||
:object-type="sidebarObjectSourceType"
|
||||
:database-type="sidebarObjectSourceDatabaseType"
|
||||
:dialect="sidebarObjectSourceDialect"
|
||||
:format-dialect="sidebarObjectSourceFormatDialect"
|
||||
:initial-editing="sidebarObjectSourceTarget.initialEditing"
|
||||
@saved="refreshSidebarActionTarget"
|
||||
/>
|
||||
|
||||
<SidebarProcedureExecutionDialog
|
||||
v-if="sidebarProcedureTarget?.connectionId && sidebarProcedureTarget.database"
|
||||
v-model:open="sidebarProcedureOpen"
|
||||
:connection-id="sidebarProcedureTarget.connectionId"
|
||||
:database="sidebarProcedureTarget.database"
|
||||
:database-type="effectiveDatabaseTypeForConnection(store.getConfig(sidebarProcedureTarget.connectionId))"
|
||||
:schema="sidebarProcedureTarget.schema"
|
||||
:routine-name="sidebarProcedureTarget.label"
|
||||
@open-sql="openSidebarProcedureSql"
|
||||
@execute="executeSidebarProcedureSql"
|
||||
/>
|
||||
|
||||
<SidebarVisibleDatabasesDialog v-if="sidebarVisibleDatabasesTarget?.connectionId" v-model:open="sidebarVisibleDatabasesOpen" :connection-id="sidebarVisibleDatabasesTarget.connectionId" :connection-name="sidebarVisibleDatabasesTarget.label" />
|
||||
|
||||
<SidebarVisibleSchemasDialog
|
||||
v-if="sidebarVisibleSchemasTarget?.connectionId && sidebarVisibleSchemasTarget.database != null"
|
||||
v-model:open="sidebarVisibleSchemasOpen"
|
||||
:connection-id="sidebarVisibleSchemasTarget.connectionId"
|
||||
:connection-name="sidebarVisibleSchemasTarget.label"
|
||||
:database="sidebarVisibleSchemasTarget.database"
|
||||
/>
|
||||
<SidebarDangerConfirmDialog
|
||||
v-if="sidebarDangerDialogRequest"
|
||||
v-model:open="sidebarDangerDialogOpen"
|
||||
:title="sidebarDangerDialogRequest.title"
|
||||
:message="sidebarDangerDialogRequest.message"
|
||||
:sql="sidebarDangerDialogRequest.sql"
|
||||
:details="sidebarDangerDialogRequest.details"
|
||||
:details-text="sidebarDangerDialogRequest.detailsText"
|
||||
:confirm-label="sidebarDangerDialogRequest.confirmLabel"
|
||||
:loading="sidebarDangerDialogConfirming || sidebarDangerDialogRequest.loading"
|
||||
:close-on-confirm="false"
|
||||
@confirm="confirmSidebarDangerDialog"
|
||||
>
|
||||
<template v-if="sidebarDangerDialogRequest.option" #options>
|
||||
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
|
||||
<input :checked="sidebarDangerDialogRequest.option.checked" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="updateSidebarDangerDialogOption" />
|
||||
<span class="grid gap-0.5">
|
||||
<span class="font-medium text-foreground">{{ sidebarDangerDialogRequest.option.label }}</span>
|
||||
<span class="text-xs leading-5 text-muted-foreground">{{ sidebarDangerDialogRequest.option.hint }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
</SidebarDangerConfirmDialog>
|
||||
<SidebarTreeItemDialogs v-if="sidebarTreeItemDialogController" :key="sidebarTreeItemDialogController.node?.id" :controller="sidebarTreeItemDialogController" @closed="sidebarTreeItemDialogController = null" />
|
||||
<InstallExtensionDialog v-if="sidebarInstallExtensionTarget" ref="sidebarInstallExtensionDialogRef" :node="sidebarInstallExtensionTarget" @close="refreshSidebarActionTarget" />
|
||||
<div v-if="store.treeNodes.length === 0" class="px-3 py-8 text-center text-muted-foreground text-xs">
|
||||
{{ t("sidebar.noConnections") }}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { AlertTriangle } from "@lucide/vue";
|
||||
|
||||
defineProps<{ error?: unknown }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/20">
|
||||
<div class="flex max-w-md items-start gap-2 rounded-md border border-destructive/40 bg-background p-4 text-sm text-destructive shadow-lg">
|
||||
<AlertTriangle class="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span class="break-all">{{ error instanceof Error ? error.message : String(error || "Dialog failed to load") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Loader2 } from "@lucide/vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/20" aria-busy="true">
|
||||
<div class="rounded-md border border-border bg-background p-4 shadow-lg">
|
||||
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,503 @@
|
||||
<script setup lang="ts">
|
||||
import { toRefs, watch } from "vue";
|
||||
import { Loader2, Clipboard, Upload } from "@lucide/vue";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
|
||||
const props = defineProps<{ controller: Record<string, any> }>();
|
||||
const emit = defineEmits<{ closed: [] }>();
|
||||
const {
|
||||
node,
|
||||
t,
|
||||
highlight,
|
||||
showDeleteConfirm,
|
||||
connectionDeleteConfirmMessage,
|
||||
confirmDelete,
|
||||
connectionDeleteMenuLabel,
|
||||
showMoveToNewGroupDialog,
|
||||
moveToNewGroupName,
|
||||
confirmMoveToNewGroup,
|
||||
showDeleteGroupConfirm,
|
||||
confirmDeleteGroup,
|
||||
showRenameObjectDialog,
|
||||
renameObjectName,
|
||||
renameObjectPreviewSql,
|
||||
renameObjectError,
|
||||
confirmRenameObject,
|
||||
showStructurePreviewDialog,
|
||||
structurePreviewTitle,
|
||||
isLoadingStructurePreview,
|
||||
structurePreviewError,
|
||||
structurePreviewSql,
|
||||
copyStructurePreview,
|
||||
saveStructurePreview,
|
||||
showStructureDocCopyDialog,
|
||||
structureDocCopyTitle,
|
||||
structureDocCopyText,
|
||||
selectTextareaContent,
|
||||
copyStructureDocText,
|
||||
showDuplicateDialog,
|
||||
duplicateTableName,
|
||||
confirmDuplicateStructure,
|
||||
showPasteDialog,
|
||||
pasteTableEntries,
|
||||
pasteTableMode,
|
||||
pasteTableDataCopySupported,
|
||||
confirmPasteTable,
|
||||
showCreateDatabaseDialog,
|
||||
createDatabaseName,
|
||||
createDatabaseCharset,
|
||||
createDatabaseCharsetOptions,
|
||||
createDatabaseCharsetLoading,
|
||||
normalizeCreateDatabaseCharset,
|
||||
createDatabaseCollation,
|
||||
createDatabaseCollationOptionsForCharset,
|
||||
createDatabaseCollationsByCharset,
|
||||
confirmCreateDatabase,
|
||||
showEditDatabasePropertiesDialog,
|
||||
editDatabasePropertiesLoading,
|
||||
editDatabaseCharset,
|
||||
editDatabaseCollation,
|
||||
canEditDatabaseComment,
|
||||
editDatabaseCommentText,
|
||||
editDatabasePropertiesPreviewSql,
|
||||
confirmEditDatabaseProperties,
|
||||
showCreateNacosNamespaceDialog,
|
||||
createNacosNamespaceId,
|
||||
createNacosNamespaceName,
|
||||
createNacosNamespaceDesc,
|
||||
createNacosNamespaceLoading,
|
||||
confirmCreateNacosNamespace,
|
||||
showEditNacosNamespaceDialog,
|
||||
editNacosNamespaceName,
|
||||
editNacosNamespaceDesc,
|
||||
editNacosNamespaceLoading,
|
||||
confirmEditNacosNamespace,
|
||||
showCreateSchemaDialog,
|
||||
createSchemaName,
|
||||
confirmCreateSchema,
|
||||
showEditSchemaCommentDialog,
|
||||
schemaCommentText,
|
||||
schemaCommentLoading,
|
||||
schemaCommentPreviewSql,
|
||||
confirmEditSchemaComment,
|
||||
canSetCreateDatabaseCharset,
|
||||
updateCreateDatabaseCharset,
|
||||
canEditDatabaseCharsetCollation,
|
||||
updateEditDatabaseCharset,
|
||||
} = toRefs(props.controller);
|
||||
|
||||
function pasteTargetsMissing(entries: Array<{ targetName: string }>): boolean {
|
||||
return entries.every((entry) => !entry.targetName.trim());
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
showDeleteConfirm,
|
||||
showMoveToNewGroupDialog,
|
||||
showDeleteGroupConfirm,
|
||||
showRenameObjectDialog,
|
||||
showStructurePreviewDialog,
|
||||
showStructureDocCopyDialog,
|
||||
showDuplicateDialog,
|
||||
showPasteDialog,
|
||||
showCreateDatabaseDialog,
|
||||
showEditDatabasePropertiesDialog,
|
||||
showCreateNacosNamespaceDialog,
|
||||
showEditNacosNamespaceDialog,
|
||||
showCreateSchemaDialog,
|
||||
showEditSchemaCommentDialog,
|
||||
],
|
||||
(open) => {
|
||||
if (open.every((value) => !value)) emit("closed");
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showDeleteConfirm">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.confirmDeleteTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ connectionDeleteConfirmMessage() }}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDeleteConfirm = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@click="
|
||||
showDeleteConfirm = false;
|
||||
confirmDelete();
|
||||
"
|
||||
>{{ connectionDeleteMenuLabel() }}</Button
|
||||
>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showMoveToNewGroupDialog">
|
||||
<DialogContent class="sm:max-w-[360px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("connectionGroup.createGroup") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input v-model="moveToNewGroupName" :placeholder="t('connectionGroup.groupNamePlaceholder')" @keydown.enter.prevent="confirmMoveToNewGroup" />
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showMoveToNewGroupDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!moveToNewGroupName.trim()" @click="confirmMoveToNewGroup">{{ t("connectionGroup.createGroup") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showDeleteGroupConfirm">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("connectionGroup.deleteGroupConfirmTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("connectionGroup.deleteGroupConfirmMessage", { name: node.label }) }}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDeleteGroupConfirm = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="destructive" @click="confirmDeleteGroup">{{ t("connectionGroup.deleteGroup") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showRenameObjectDialog">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.renameObjectTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<Input v-model="renameObjectName" :placeholder="t('contextMenu.renameObjectNamePlaceholder')" @keydown.enter.prevent="confirmRenameObject" />
|
||||
<pre v-if="renameObjectPreviewSql" class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap" v-html="highlight(renameObjectPreviewSql)"></pre>
|
||||
<p v-if="renameObjectError" class="text-sm text-destructive">{{ renameObjectError }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showRenameObjectDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!renameObjectName.trim() || renameObjectName.trim() === node.label" @click="confirmRenameObject">
|
||||
{{ t("contextMenu.renameObject") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showStructurePreviewDialog">
|
||||
<DialogContent class="sm:max-w-[760px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ structurePreviewTitle || t("contextMenu.exportStructure") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<div v-if="isLoadingStructurePreview" class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
<span>{{ t("contextMenu.exportStructureLoading") }}</span>
|
||||
</div>
|
||||
<p v-else-if="structurePreviewError" class="text-sm text-destructive">{{ structurePreviewError }}</p>
|
||||
<pre v-else class="max-h-[56vh] min-h-64 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap" v-html="highlight(structurePreviewSql)"></pre>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showStructurePreviewDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="outline" :disabled="isLoadingStructurePreview || !structurePreviewSql" @click="copyStructurePreview">
|
||||
<Clipboard class="h-4 w-4" />
|
||||
{{ t("contextMenu.copyStructure") }}
|
||||
</Button>
|
||||
<Button :disabled="isLoadingStructurePreview || !structurePreviewSql" @click="saveStructurePreview">
|
||||
<Upload class="h-4 w-4" />
|
||||
{{ t("contextMenu.saveStructure") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showStructureDocCopyDialog">
|
||||
<DialogContent class="sm:max-w-[760px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ structureDocCopyTitle || t("contextMenu.copyStructureAs") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<p class="text-sm text-muted-foreground">{{ t("contextMenu.structureDocCopyFallbackHint") }}</p>
|
||||
<textarea readonly class="max-h-[56vh] min-h-64 resize-y overflow-auto rounded bg-muted p-3 font-mono text-xs whitespace-pre" :value="structureDocCopyText" @focus="selectTextareaContent"></textarea>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showStructureDocCopyDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!structureDocCopyText" @click="copyStructureDocText">
|
||||
<Clipboard class="h-4 w-4" />
|
||||
{{ t("contextMenu.copyStructure") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showDuplicateDialog">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.duplicateNameTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input v-model="duplicateTableName" :placeholder="t('contextMenu.duplicateNamePlaceholder')" @keydown.enter.prevent="confirmDuplicateStructure" />
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDuplicateDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!duplicateTableName.trim()" @click="confirmDuplicateStructure">{{ t("dangerDialog.confirm") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showPasteDialog">
|
||||
<DialogContent class="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ pasteTableEntries.length > 1 ? t("contextMenu.batchPasteTitle") : t("contextMenu.pasteTableConfirmTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer" :class="{ 'opacity-50 cursor-not-allowed': !pasteTableDataCopySupported }">
|
||||
<input v-model="pasteTableMode" type="radio" value="structure-and-data" class="accent-primary" :disabled="!pasteTableDataCopySupported" />
|
||||
{{ t("contextMenu.pasteOptionStructureAndData") }}
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||||
<input v-model="pasteTableMode" type="radio" value="structure-only" class="accent-primary" />
|
||||
{{ t("contextMenu.pasteOptionStructureOnly") }}
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer" :class="{ 'opacity-50 cursor-not-allowed': !pasteTableDataCopySupported }">
|
||||
<input v-model="pasteTableMode" type="radio" value="data-only" class="accent-primary" :disabled="!pasteTableDataCopySupported" />
|
||||
{{ t("contextMenu.pasteOptionDataOnly") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<div v-for="(entry, idx) in pasteTableEntries" :key="idx" class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground truncate min-w-0 flex-shrink basis-1/3" :title="entry.sourceName">{{ entry.sourceName }}</span>
|
||||
<span class="text-xs text-muted-foreground flex-shrink-0">→</span>
|
||||
<Input v-model="entry.targetName" class="flex-1 h-8 text-sm" :placeholder="t('contextMenu.duplicateNamePlaceholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showPasteDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="pasteTargetsMissing(pasteTableEntries)" @click="confirmPasteTable">{{ t("dangerDialog.confirm") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showCreateDatabaseDialog">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.createDatabase") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input v-model="createDatabaseName" :placeholder="t('contextMenu.createDatabaseNamePlaceholder')" @keydown.enter.prevent="confirmCreateDatabase" />
|
||||
<div v-if="canSetCreateDatabaseCharset" class="grid gap-2">
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCharset") }}</label>
|
||||
<SearchableSelect
|
||||
:model-value="createDatabaseCharset"
|
||||
:options="createDatabaseCharsetOptions"
|
||||
:placeholder="t('contextMenu.createDatabaseCharsetPlaceholder')"
|
||||
:search-placeholder="t('contextMenu.createDatabaseCharsetSearchPlaceholder')"
|
||||
:empty-text="t('contextMenu.createDatabaseCharsetEmpty')"
|
||||
:loading-text="t('contextMenu.createDatabaseCharsetLoading')"
|
||||
:loading="createDatabaseCharsetLoading"
|
||||
:normalize-custom="normalizeCreateDatabaseCharset"
|
||||
allow-custom
|
||||
trigger-variant="outline"
|
||||
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
|
||||
content-class="w-[var(--reka-popover-trigger-width)]"
|
||||
@update:model-value="updateCreateDatabaseCharset"
|
||||
>
|
||||
<template #custom-option-label="{ value }">
|
||||
<span class="truncate">{{ t("contextMenu.createDatabaseCharsetCustomOption", { value }) }}</span>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCollation") }}</label>
|
||||
<SearchableSelect
|
||||
v-model="createDatabaseCollation"
|
||||
:options="createDatabaseCollationOptionsForCharset(createDatabaseCharset, createDatabaseCollationsByCharset)"
|
||||
:placeholder="t('contextMenu.createDatabaseCollationPlaceholder')"
|
||||
:search-placeholder="t('contextMenu.createDatabaseCollationSearchPlaceholder')"
|
||||
:empty-text="t('contextMenu.createDatabaseCollationEmpty')"
|
||||
:loading-text="t('contextMenu.createDatabaseCollationLoading')"
|
||||
:loading="createDatabaseCharsetLoading"
|
||||
:normalize-custom="normalizeCreateDatabaseCharset"
|
||||
allow-custom
|
||||
trigger-variant="outline"
|
||||
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
|
||||
content-class="w-[var(--reka-popover-trigger-width)]"
|
||||
>
|
||||
<template #custom-option-label="{ value }">
|
||||
<span class="truncate">{{ t("contextMenu.createDatabaseCollationCustomOption", { value }) }}</span>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showCreateDatabaseDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!createDatabaseName.trim()" @click="confirmCreateDatabase">{{ t("dangerDialog.confirm") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showEditDatabasePropertiesDialog">
|
||||
<DialogContent class="sm:max-w-[460px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.editDatabasePropertiesTitle", { name: node.label }) }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<div v-if="canEditDatabaseCharsetCollation" class="grid gap-3">
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCharset") }}</label>
|
||||
<SearchableSelect
|
||||
:model-value="editDatabaseCharset"
|
||||
:options="createDatabaseCharsetOptions"
|
||||
:placeholder="t('contextMenu.createDatabaseCharsetPlaceholder')"
|
||||
:search-placeholder="t('contextMenu.createDatabaseCharsetSearchPlaceholder')"
|
||||
:empty-text="t('contextMenu.createDatabaseCharsetEmpty')"
|
||||
:loading-text="t('contextMenu.createDatabaseCharsetLoading')"
|
||||
:loading="createDatabaseCharsetLoading"
|
||||
:normalize-custom="normalizeCreateDatabaseCharset"
|
||||
allow-custom
|
||||
trigger-variant="outline"
|
||||
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
|
||||
content-class="w-[var(--reka-popover-trigger-width)]"
|
||||
@update:model-value="updateEditDatabaseCharset"
|
||||
>
|
||||
<template #custom-option-label="{ value }">
|
||||
<span class="truncate">{{ t("contextMenu.createDatabaseCharsetCustomOption", { value }) }}</span>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCollation") }}</label>
|
||||
<SearchableSelect
|
||||
v-model="editDatabaseCollation"
|
||||
:options="createDatabaseCollationOptionsForCharset(editDatabaseCharset, createDatabaseCollationsByCharset)"
|
||||
:placeholder="t('contextMenu.createDatabaseCollationPlaceholder')"
|
||||
:search-placeholder="t('contextMenu.createDatabaseCollationSearchPlaceholder')"
|
||||
:empty-text="t('contextMenu.createDatabaseCollationEmpty')"
|
||||
:loading-text="t('contextMenu.createDatabaseCollationLoading')"
|
||||
:loading="createDatabaseCharsetLoading"
|
||||
:normalize-custom="normalizeCreateDatabaseCharset"
|
||||
allow-custom
|
||||
trigger-variant="outline"
|
||||
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
|
||||
content-class="w-[var(--reka-popover-trigger-width)]"
|
||||
>
|
||||
<template #custom-option-label="{ value }">
|
||||
<span class="truncate">{{ t("contextMenu.createDatabaseCollationCustomOption", { value }) }}</span>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="canEditDatabaseComment" class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.editDatabaseComment") }}</label>
|
||||
<textarea
|
||||
v-model="editDatabaseCommentText"
|
||||
class="min-h-28 w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/40"
|
||||
:placeholder="t('contextMenu.editDatabaseCommentPlaceholder')"
|
||||
:disabled="editDatabasePropertiesLoading"
|
||||
@keydown.meta.enter.prevent="confirmEditDatabaseProperties"
|
||||
@keydown.ctrl.enter.prevent="confirmEditDatabaseProperties"
|
||||
></textarea>
|
||||
</div>
|
||||
<pre v-if="editDatabasePropertiesPreviewSql" class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap" v-html="highlight(editDatabasePropertiesPreviewSql)"></pre>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="editDatabasePropertiesLoading" @click="showEditDatabasePropertiesDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="editDatabasePropertiesLoading" @click="confirmEditDatabaseProperties">
|
||||
{{ editDatabasePropertiesLoading ? t("contextMenu.editDatabasePropertiesSaving") : t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showCreateNacosNamespaceDialog">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.createNamespace") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceId") }}</label>
|
||||
<Input v-model="createNacosNamespaceId" :placeholder="t('nacos.namespaceIdPlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceName") }}</label>
|
||||
<Input v-model="createNacosNamespaceName" :placeholder="t('nacos.namespaceNamePlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceDesc") }}</label>
|
||||
<Input v-model="createNacosNamespaceDesc" :placeholder="t('nacos.namespaceDescPlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="createNacosNamespaceLoading" @click="showCreateNacosNamespaceDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!createNacosNamespaceName.trim() || createNacosNamespaceLoading" @click="confirmCreateNacosNamespace">
|
||||
{{ createNacosNamespaceLoading ? t("nacos.creatingNamespace") : t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showEditNacosNamespaceDialog">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.editNamespace") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceName") }}</label>
|
||||
<Input v-model="editNacosNamespaceName" :placeholder="t('nacos.namespaceNamePlaceholder')" @keydown.enter.prevent="confirmEditNacosNamespace" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceDesc") }}</label>
|
||||
<Input v-model="editNacosNamespaceDesc" :placeholder="t('nacos.namespaceDescPlaceholder')" @keydown.enter.prevent="confirmEditNacosNamespace" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="editNacosNamespaceLoading" @click="showEditNacosNamespaceDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!editNacosNamespaceName.trim() || editNacosNamespaceLoading" @click="confirmEditNacosNamespace">
|
||||
{{ editNacosNamespaceLoading ? t("nacos.updatingNamespace") : t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showCreateSchemaDialog">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.createSchema") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input v-model="createSchemaName" :placeholder="t('contextMenu.createSchemaNamePlaceholder')" @keydown.enter.prevent="confirmCreateSchema" />
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showCreateSchemaDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!createSchemaName.trim()" @click="confirmCreateSchema">{{ t("dangerDialog.confirm") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showEditSchemaCommentDialog">
|
||||
<DialogContent class="sm:max-w-[520px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.editSchemaCommentTitle", { name: node.label }) }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<textarea
|
||||
v-model="schemaCommentText"
|
||||
class="min-h-28 w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/40"
|
||||
:placeholder="t('contextMenu.schemaCommentPlaceholder')"
|
||||
:disabled="schemaCommentLoading"
|
||||
@keydown.meta.enter.prevent="confirmEditSchemaComment"
|
||||
@keydown.ctrl.enter.prevent="confirmEditSchemaComment"
|
||||
></textarea>
|
||||
<pre v-if="schemaCommentPreviewSql" class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap" v-html="highlight(schemaCommentPreviewSql)"></pre>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="schemaCommentLoading" @click="showEditSchemaCommentDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="schemaCommentLoading" @click="confirmEditSchemaComment">
|
||||
{{ schemaCommentLoading ? t("contextMenu.schemaCommentSaving") : t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,24 @@
|
||||
import { defineAsyncComponent, type Component } from "vue";
|
||||
import SidebarAsyncDialogError from "./SidebarAsyncDialogError.vue";
|
||||
import SidebarAsyncDialogLoading from "./SidebarAsyncDialogLoading.vue";
|
||||
|
||||
function lazySidebarDialog(loader: () => Promise<Component>) {
|
||||
return defineAsyncComponent({
|
||||
loader,
|
||||
loadingComponent: SidebarAsyncDialogLoading,
|
||||
errorComponent: SidebarAsyncDialogError,
|
||||
delay: 120,
|
||||
timeout: 15_000,
|
||||
onError(_error, retry, fail, attempts) {
|
||||
if (attempts < 2) retry();
|
||||
else fail();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const SidebarDangerConfirmDialog = lazySidebarDialog(() => import("@/components/editor/DangerConfirmDialog.vue"));
|
||||
export const SidebarVisibleDatabasesDialog = lazySidebarDialog(() => import("@/components/sidebar/VisibleDatabasesDialog.vue"));
|
||||
export const SidebarVisibleSchemasDialog = lazySidebarDialog(() => import("@/components/sidebar/VisibleSchemasDialog.vue"));
|
||||
export const SidebarDdlViewDialog = lazySidebarDialog(() => import("@/components/objects/DdlViewDialog.vue"));
|
||||
export const SidebarObjectSourceDialog = lazySidebarDialog(() => import("@/components/objects/ObjectSourceDialog.vue"));
|
||||
export const SidebarProcedureExecutionDialog = lazySidebarDialog(() => import("@/components/objects/ProcedureExecutionDialog.vue"));
|
||||
@@ -0,0 +1,139 @@
|
||||
import { ref, shallowRef } from "vue";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import type { PasteTableMode } from "@/lib/table/tableClipboard";
|
||||
import { fallbackCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
|
||||
export type DuplicateStructureSource = TreeNode & { connectionId: string; database: string };
|
||||
type ConnectionDeleteTarget = TreeNode & { connectionId: string };
|
||||
|
||||
export const fallbackCreateDatabaseCharset = fallbackCreateDatabaseCharsetMetadata();
|
||||
|
||||
export const sidebarTreeDialogOwner = shallowRef<symbol | null>(null);
|
||||
export const sidebarDangerTarget = shallowRef<TreeNode | null>(null);
|
||||
export const sidebarFormTarget = shallowRef<TreeNode | null>(null);
|
||||
export const connectionDeleteTargetSnapshot = ref<ConnectionDeleteTarget[]>([]);
|
||||
export const showDeleteConfirm = ref(false);
|
||||
export const showDropTableConfirm = ref(false);
|
||||
export const showDropTableChildObjectConfirm = ref(false);
|
||||
export const showBatchDropConfirm = ref(false);
|
||||
export const showBatchEmptyConfirm = ref(false);
|
||||
export const showBatchTruncateConfirm = ref(false);
|
||||
export const showStructurePreviewDialog = ref(false);
|
||||
export const showStructureDocCopyDialog = ref(false);
|
||||
export const structurePreviewSql = ref("");
|
||||
export const structurePreviewTitle = ref("");
|
||||
export const structurePreviewDefaultFileName = ref("structure.sql");
|
||||
export const structurePreviewError = ref("");
|
||||
export const structureDocCopyText = ref("");
|
||||
export const structureDocCopyTitle = ref("");
|
||||
export const isLoadingStructurePreview = ref(false);
|
||||
export const showEmptyTableConfirm = ref(false);
|
||||
export const showTruncateTableConfirm = ref(false);
|
||||
export const showRenameObjectDialog = ref(false);
|
||||
export const renameObjectName = ref("");
|
||||
export const renameObjectError = ref("");
|
||||
export const renameObjectPreviewSql = ref("");
|
||||
export const dropTablePreviewSql = ref("");
|
||||
export const dropTableCascade = ref(false);
|
||||
export const batchDropCascade = ref(false);
|
||||
export const emptyTablePreviewSql = ref("");
|
||||
export const truncateTablePreviewSql = ref("");
|
||||
export const truncateTableCascade = ref(false);
|
||||
export const dropObjectPreviewSql = ref("");
|
||||
export const showDropObjectConfirm = ref(false);
|
||||
export const dropTableChildObjectPreviewSql = ref("");
|
||||
export const batchDropPreviewSql = ref("");
|
||||
export const batchEmptyPreviewSql = ref("");
|
||||
export const batchEmptyTargets = ref<TreeNode[]>([]);
|
||||
export const batchDropTargets = ref<TreeNode[]>([]);
|
||||
export const batchTruncateTargets = ref<TreeNode[]>([]);
|
||||
export const batchTruncatePreviewSql = ref("");
|
||||
export const batchTruncateCascade = ref(false);
|
||||
export const dropDatabasePreviewSql = ref("");
|
||||
export const dropSchemaPreviewSql = ref("");
|
||||
export const showDuplicateDialog = ref(false);
|
||||
export const duplicateTableName = ref("");
|
||||
export const duplicateStructureSource = ref<DuplicateStructureSource | null>(null);
|
||||
export const showPasteDialog = ref(false);
|
||||
export const pasteTableMode = ref<PasteTableMode>("structure-and-data");
|
||||
export const pasteTableEntries = ref<Array<{ sourceName: string; targetName: string; connectionId: string; database: string; schema?: string }>>([]);
|
||||
export const showCreateDatabaseDialog = ref(false);
|
||||
export const createDatabaseName = ref("");
|
||||
export const createDatabaseCharset = ref("utf8mb4");
|
||||
export const createDatabaseCollation = ref("utf8mb4_unicode_ci");
|
||||
export const showCreateNacosNamespaceDialog = ref(false);
|
||||
export const createNacosNamespaceId = ref("");
|
||||
export const createNacosNamespaceName = ref("");
|
||||
export const createNacosNamespaceDesc = ref("");
|
||||
export const createNacosNamespaceLoading = ref(false);
|
||||
export const showEditNacosNamespaceDialog = ref(false);
|
||||
export const editNacosNamespaceName = ref("");
|
||||
export const editNacosNamespaceDesc = ref("");
|
||||
export const editNacosNamespaceLoading = ref(false);
|
||||
export const createDatabaseCharsetOptions = ref<string[]>(fallbackCreateDatabaseCharset.charsets);
|
||||
export const createDatabaseCollationsByCharset = ref<Record<string, string[]>>(fallbackCreateDatabaseCharset.collationsByCharset);
|
||||
export const createDatabaseCharsetLoading = ref(false);
|
||||
export const showDropDatabaseConfirm = ref(false);
|
||||
export const dropDatabaseLoading = ref(false);
|
||||
export const showDropMongoCollectionConfirm = ref(false);
|
||||
export const dropMongoCollectionLoading = ref(false);
|
||||
export const showDropMongoIndexConfirm = ref(false);
|
||||
export const dropMongoIndexLoading = ref(false);
|
||||
export const showDropAllMongoIndexesConfirm = ref(false);
|
||||
export const dropAllMongoIndexesLoading = ref(false);
|
||||
export const showFlushRedisDbConfirm = ref(false);
|
||||
export const showCreateSchemaDialog = ref(false);
|
||||
export const createSchemaName = ref("");
|
||||
export const showDropSchemaConfirm = ref(false);
|
||||
export const showEditDatabasePropertiesDialog = ref(false);
|
||||
export const editDatabasePropertiesLoading = ref(false);
|
||||
export const editDatabasePropertiesPreviewSql = ref("");
|
||||
export const editDatabaseCharset = ref("utf8mb4");
|
||||
export const editDatabaseCollation = ref("utf8mb4_unicode_ci");
|
||||
export const editDatabaseCommentText = ref("");
|
||||
export const showEditSchemaCommentDialog = ref(false);
|
||||
export const schemaCommentText = ref("");
|
||||
export const schemaCommentLoading = ref(false);
|
||||
export const schemaCommentPreviewSql = ref("");
|
||||
export const showDeleteGroupConfirm = ref(false);
|
||||
export const showMoveToNewGroupDialog = ref(false);
|
||||
export const moveToNewGroupName = ref("");
|
||||
|
||||
const openFlags = [
|
||||
showDeleteConfirm,
|
||||
showDropTableConfirm,
|
||||
showDropTableChildObjectConfirm,
|
||||
showBatchDropConfirm,
|
||||
showBatchEmptyConfirm,
|
||||
showBatchTruncateConfirm,
|
||||
showStructurePreviewDialog,
|
||||
showStructureDocCopyDialog,
|
||||
showEmptyTableConfirm,
|
||||
showTruncateTableConfirm,
|
||||
showDropObjectConfirm,
|
||||
showRenameObjectDialog,
|
||||
showDuplicateDialog,
|
||||
showPasteDialog,
|
||||
showCreateDatabaseDialog,
|
||||
showCreateNacosNamespaceDialog,
|
||||
showEditNacosNamespaceDialog,
|
||||
showDropDatabaseConfirm,
|
||||
showDropMongoCollectionConfirm,
|
||||
showDropMongoIndexConfirm,
|
||||
showDropAllMongoIndexesConfirm,
|
||||
showFlushRedisDbConfirm,
|
||||
showCreateSchemaDialog,
|
||||
showDropSchemaConfirm,
|
||||
showEditDatabasePropertiesDialog,
|
||||
showEditSchemaCommentDialog,
|
||||
showDeleteGroupConfirm,
|
||||
showMoveToNewGroupDialog,
|
||||
];
|
||||
|
||||
export function resetSidebarTreeDialogState() {
|
||||
for (const flag of openFlags) flag.value = false;
|
||||
sidebarTreeDialogOwner.value = null;
|
||||
sidebarDangerTarget.value = null;
|
||||
sidebarFormTarget.value = null;
|
||||
connectionDeleteTargetSnapshot.value = [];
|
||||
}
|
||||
@@ -70,6 +70,8 @@ function close() {
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ close });
|
||||
|
||||
function onPointerDownOutside(e: PointerEvent) {
|
||||
// Only respond to primary (left) button presses. This avoids a macOS
|
||||
// issue where Ctrl+right-click generates a synthetic click event on
|
||||
@@ -125,9 +127,12 @@ function handleSubItemClick(item: ContextMenuItem) {
|
||||
item.action?.();
|
||||
}
|
||||
|
||||
function onContextMenu(event: MouseEvent) {
|
||||
function onContextMenu(event: MouseEvent, itemsOverride?: ContextMenuItem[]) {
|
||||
// Some callers build large context menus; resolve them only for actual opens.
|
||||
const items = typeof props.items === "function" ? props.items() : props.items;
|
||||
// Tree-level hosts may replace their items and open in the same event turn.
|
||||
// Accepting the resolved items directly avoids reading the previous prop
|
||||
// value before Vue has flushed the parent-to-child update.
|
||||
const items = itemsOverride ?? (typeof props.items === "function" ? props.items() : props.items);
|
||||
if (items.length === 0) return;
|
||||
activeItems.value = items;
|
||||
event.preventDefault();
|
||||
|
||||
Reference in new issue
Block a user