From 53a45330a6ef8815248e5267cd6a16afc11a211c Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 12 May 2026 14:33:52 +0800 Subject: [PATCH] feat(sidebar): group objects by type and add Object Browser context menus (#222) Sidebar tree now groups database objects into Tables, Views, Procedures, and Functions folders instead of a flat list. Empty groups are hidden and each group shows its item count. Object Browser rows now have right-click context menus with actions like View Data, New Query, View Source, and Drop. --- src/components/objects/ObjectBrowser.vue | 162 ++++++++++++++++++++--- src/components/sidebar/TreeItem.vue | 101 ++++++++++++++ src/i18n/locales/en.ts | 17 +++ src/i18n/locales/es.ts | 17 +++ src/i18n/locales/zh-CN.ts | 17 +++ src/lib/sidebarTreeItemLayout.ts | 2 + src/lib/tableTree.ts | 85 +++++++++++- src/stores/connectionStore.ts | 27 ++-- src/types/database.ts | 6 + 9 files changed, 407 insertions(+), 27 deletions(-) diff --git a/src/components/objects/ObjectBrowser.vue b/src/components/objects/ObjectBrowser.vue index 66f135d3e8..cfca050f40 100644 --- a/src/components/objects/ObjectBrowser.vue +++ b/src/components/objects/ObjectBrowser.vue @@ -12,17 +12,29 @@ import { Search, ScrollText, Table2, + TerminalSquare, + Trash2, X, } from "lucide-vue-next"; import { useI18n } from "vue-i18n"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import * as api from "@/lib/api"; import type { ConnectionConfig, ObjectInfo, ObjectSourceKind } from "@/types/database"; import { isSchemaAware } from "@/lib/databaseCapabilities"; +import { qualifiedTableName } from "@/lib/tableSelectSql"; import { useToast } from "@/composables/useToast"; import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor"; +import { useQueryStore } from "@/stores/queryStore"; import QueryEditor from "@/components/editor/QueryEditor.vue"; import type { SqlFormatDialect } from "@/lib/sqlFormatter"; @@ -49,6 +61,7 @@ const emit = defineEmits<{ const { t } = useI18n(); const { toast } = useToast(); +const queryStore = useQueryStore(); const schemas = ref([]); const selectedSchema = ref(props.schema); @@ -66,6 +79,8 @@ const sourceDraft = ref(""); const sourceSaving = ref(false); const sourceSaveError = ref(""); const error = ref(""); +const showDropConfirm = ref(false); +const dropTarget = ref(null); let loadId = 0; const needsSchema = computed(() => isSchemaAware(props.connection.db_type)); @@ -196,6 +211,73 @@ async function openSource(row: ObjectRow) { } } +function openNewQuery(row: ObjectRow) { + const tabId = queryStore.createTab(props.connection.id, props.database, row.name); + queryStore.updateSql(tabId, `SELECT * FROM ${qualifiedName(row)} LIMIT 100;`); +} + +function qualifiedName(row: ObjectRow): string { + return qualifiedTableName({ + databaseType: props.connection.db_type, + schema: row.schema || selectedSchema.value, + tableName: row.name, + }); +} + +function requestDrop(row: ObjectRow) { + dropTarget.value = row; + showDropConfirm.value = true; +} + +async function confirmDrop() { + if (!dropTarget.value) return; + const row = dropTarget.value; + const typeSql = + row.type === "VIEW" + ? "VIEW" + : row.type === "PROCEDURE" + ? "PROCEDURE" + : row.type === "FUNCTION" + ? "FUNCTION" + : "TABLE"; + const name = qualifiedName(row); + try { + await api.executeQuery(props.connection.id, props.database, `DROP ${typeSql} ${name}`); + const successKey = + row.type === "VIEW" + ? "contextMenu.dropViewSuccess" + : row.type === "PROCEDURE" + ? "contextMenu.dropProcedureSuccess" + : row.type === "FUNCTION" + ? "contextMenu.dropFunctionSuccess" + : "contextMenu.dropTableSuccess"; + toast(t(successKey, { name: row.name })); + await reload(); + } catch (e: any) { + toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000); + } + dropTarget.value = null; +} + +function dropConfirmTitle(): string { + if (!dropTarget.value) return ""; + const type = dropTarget.value.type; + if (type === "VIEW") return t("contextMenu.confirmDropViewTitle"); + if (type === "PROCEDURE") return t("contextMenu.confirmDropProcedureTitle"); + if (type === "FUNCTION") return t("contextMenu.confirmDropFunctionTitle"); + return t("contextMenu.confirmDropTableTitle"); +} + +function dropConfirmMessage(): string { + if (!dropTarget.value) return ""; + const name = dropTarget.value.name; + const type = dropTarget.value.type; + if (type === "VIEW") return t("contextMenu.confirmDropViewMessage", { name }); + if (type === "PROCEDURE") return t("contextMenu.confirmDropProcedureMessage", { name }); + if (type === "FUNCTION") return t("contextMenu.confirmDropFunctionMessage", { name }); + return t("contextMenu.confirmDropTableMessage", { name }); +} + function closeSource() { sourceRow.value = null; sourceContent.value = ""; @@ -417,22 +499,62 @@ watch( key-field="id" >
@@ -520,6 +642,14 @@ watch(
+ +