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.
This commit is contained in:
t8y2
2026-05-12 14:33:52 +08:00
parent 76ff987d00
commit 53a45330a6
9 changed files with 407 additions and 27 deletions
+146 -16
View File
@@ -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<string[]>([]);
const selectedSchema = ref<string | undefined>(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<ObjectRow | null>(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"
>
<template #default="{ item }">
<div
class="grid h-[38px] cursor-pointer items-center gap-3 border-b px-3 hover:bg-accent/50"
:class="{ 'bg-accent/40': sourceRow?.id === item.id }"
:style="{ gridTemplateColumns }"
@click="openRow(item)"
>
<div class="flex min-w-0 items-center gap-2">
<component :is="iconFor(item)" class="h-3.5 w-3.5 shrink-0" :class="iconClass(item.type)" />
<span class="truncate text-[13px] font-medium text-foreground">{{ item.name }}</span>
</div>
<div class="truncate text-xs text-muted-foreground">{{ typeLabel(item.type) }}</div>
<div class="truncate text-xs text-muted-foreground">{{ item.schema || props.database }}</div>
<div v-if="hasComments" class="truncate text-xs text-muted-foreground" :title="item.comment || ''">
{{ item.comment || "" }}
</div>
</div>
<ContextMenu>
<ContextMenuTrigger as-child>
<div
class="grid h-[38px] cursor-pointer items-center gap-3 border-b px-3 hover:bg-accent/50"
:class="{ 'bg-accent/40': sourceRow?.id === item.id }"
:style="{ gridTemplateColumns }"
@click="openRow(item)"
>
<div class="flex min-w-0 items-center gap-2">
<component :is="iconFor(item)" class="h-3.5 w-3.5 shrink-0" :class="iconClass(item.type)" />
<span class="truncate text-[13px] font-medium text-foreground">{{ item.name }}</span>
</div>
<div class="truncate text-xs text-muted-foreground">{{ typeLabel(item.type) }}</div>
<div class="truncate text-xs text-muted-foreground">{{ item.schema || props.database }}</div>
<div v-if="hasComments" class="truncate text-xs text-muted-foreground" :title="item.comment || ''">
{{ item.comment || "" }}
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent class="w-auto min-w-40">
<!-- TABLE -->
<template v-if="item.type === 'TABLE'">
<ContextMenuItem @click="openRow(item)">
<Table2 class="w-4 h-4 mr-2" /> {{ t("contextMenu.viewData") }}
</ContextMenuItem>
<ContextMenuItem @click="openNewQuery(item)">
<TerminalSquare class="w-4 h-4 mr-2" /> {{ t("contextMenu.newQuery") }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDrop(item)">
<Trash2 class="w-4 h-4 mr-2" /> {{ t("contextMenu.dropTable") }}
</ContextMenuItem>
</template>
<!-- VIEW -->
<template v-else-if="item.type === 'VIEW'">
<ContextMenuItem @click="openSource(item)">
<Code2 class="w-4 h-4 mr-2" /> {{ t("contextMenu.viewSource") }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDrop(item)">
<Trash2 class="w-4 h-4 mr-2" /> {{ t("contextMenu.dropView") }}
</ContextMenuItem>
</template>
<!-- PROCEDURE / FUNCTION -->
<template v-else>
<ContextMenuItem @click="openSource(item)">
<Code2 class="w-4 h-4 mr-2" /> {{ t("contextMenu.viewSource") }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDrop(item)">
<Trash2 class="w-4 h-4 mr-2" />
{{ item.type === "PROCEDURE" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction") }}
</ContextMenuItem>
</template>
</ContextMenuContent>
</ContextMenu>
</template>
</RecycleScroller>
<div v-if="sourceRow" class="flex h-[42%] min-h-44 shrink-0 flex-col border-t bg-background">
@@ -520,6 +642,14 @@ watch(
</div>
</div>
</div>
<DangerConfirmDialog
v-model:open="showDropConfirm"
:title="dropConfirmTitle()"
:details="dropConfirmMessage()"
:confirm-label="t('dangerDialog.deleteConfirm')"
@confirm="confirmDrop"
/>
</template>
<style scoped>
+101
View File
@@ -38,6 +38,9 @@ import {
CopyPlus,
Plus,
FileText,
ScrollText,
Braces,
Code2,
} from "lucide-vue-next";
import {
ContextMenu,
@@ -182,6 +185,18 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Database, colorClass: "text-yellow-500" };
case "mongo-collection":
return { icon: Table, colorClass: "text-green-400" };
case "procedure":
return { icon: ScrollText, colorClass: "text-blue-500" };
case "function":
return { icon: Braces, colorClass: "text-amber-500" };
case "group-tables":
return { icon: Table, colorClass: "text-green-500" };
case "group-views":
return { icon: Eye, colorClass: "text-purple-500" };
case "group-procedures":
return { icon: ScrollText, colorClass: "text-blue-500" };
case "group-functions":
return { icon: Braces, colorClass: "text-amber-500" };
default:
return { icon: Database, colorClass: "text-muted-foreground" };
}
@@ -192,6 +207,10 @@ const groupTypes: Set<TreeNodeType> = new Set([
"group-indexes",
"group-fkeys",
"group-triggers",
"group-tables",
"group-views",
"group-procedures",
"group-functions",
"saved-sql-root",
"saved-sql-folder",
]);
@@ -232,6 +251,16 @@ async function toggle() {
return;
}
if (
node.type === "group-tables" ||
node.type === "group-views" ||
node.type === "group-procedures" ||
node.type === "group-functions"
) {
node.isExpanded = !node.isExpanded;
return;
}
if (node.isExpanded) {
node.isExpanded = false;
return;
@@ -512,6 +541,38 @@ const showCreateSchemaDialog = ref(false);
const createSchemaName = ref("");
const showDropSchemaConfirm = ref(false);
// --- Procedure / Function Management ---
const showDropObjectConfirm = ref(false);
function buildDropObjectSql(): string {
const node = props.node;
const keyword = node.type === "procedure" ? "PROCEDURE" : "FUNCTION";
const name = qualifiedTableName(node.label, node.schema);
return `DROP ${keyword} ${name};`;
}
function viewObjectSource() {
void openObjectBrowser();
}
function requestDropObject() {
showDropObjectConfirm.value = true;
}
async function confirmDropObject() {
const node = props.node;
if (!node.connectionId || !node.database) return;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.executeQuery(node.connectionId, node.database, buildDropObjectSql(), node.schema);
const msgKey = node.type === "procedure" ? "contextMenu.dropProcedureSuccess" : "contextMenu.dropFunctionSuccess";
toast(t(msgKey, { name: node.label }), 3000);
await refreshTableList(node);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
}
const isTableNotView = computed(() => props.node.type === "table");
const supportsTruncate = computed(() => {
@@ -1190,6 +1251,8 @@ const hasTypeMenu = computed(() => {
t === "table" ||
t === "view" ||
t === "column" ||
t === "procedure" ||
t === "function" ||
t === "saved-sql-root" ||
t === "saved-sql-folder" ||
t === "saved-sql-file" ||
@@ -1471,6 +1534,17 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
@vue:mounted="($event: any) => $event.el.focus()"
/>
<span v-else class="min-w-0 flex-1 truncate">{{ displayLabel(node) }}</span>
<span
v-if="
(node.type === 'group-tables' ||
node.type === 'group-views' ||
node.type === 'group-procedures' ||
node.type === 'group-functions') &&
node.objectCount != null
"
class="text-muted-foreground text-[10px] shrink-0"
>{{ node.objectCount }}</span
>
<Badge v-if="isNodeDefaultDatabase" variant="secondary" class="h-4 px-1.5 text-[10px]">
{{ t("editor.defaultDatabase") }}
</Badge>
@@ -1711,6 +1785,18 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
</ContextMenuItem>
</template>
<template v-if="node.type === 'procedure' || node.type === 'function'">
<ContextMenuItem @click="viewObjectSource">
<Code2 class="w-4 h-4 mr-2" />
{{ t("contextMenu.viewSource") }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem class="text-destructive" @click="requestDropObject">
<Trash2 class="w-4 h-4 mr-2" />
{{ node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction") }}
</ContextMenuItem>
</template>
<template v-if="isGroupLabel(node)">
<ContextMenuItem v-if="node.type === 'saved-sql-root'" @click="openCreateSavedSqlFolder">
<FolderPlus class="w-4 h-4" /> {{ t("savedSql.newFolder") }}
@@ -1887,6 +1973,21 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
@confirm="confirmTruncateTable"
/>
<DangerConfirmDialog
v-model:open="showDropObjectConfirm"
:title="
node.type === 'procedure' ? t('contextMenu.confirmDropProcedureTitle') : t('contextMenu.confirmDropFunctionTitle')
"
:message="
node.type === 'procedure'
? t('contextMenu.confirmDropProcedureMessage', { name: node.label })
: t('contextMenu.confirmDropFunctionMessage', { name: node.label })
"
:sql="buildDropObjectSql()"
:confirm-label="node.type === 'procedure' ? t('contextMenu.dropProcedure') : t('contextMenu.dropFunction')"
@confirm="confirmDropObject"
/>
<Dialog v-model:open="showDuplicateDialog">
<DialogContent class="sm:max-w-[400px]">
<DialogHeader>
+17
View File
@@ -544,6 +544,19 @@ export default {
confirmTruncateTableTitle: "Truncate Table",
confirmTruncateTableMessage: 'Are you sure you want to truncate "{name}"? This will remove all rows.',
dropTableSuccess: 'Table "{name}" dropped',
viewSource: "View Source",
dropView: "Drop View",
dropProcedure: "Drop Procedure",
dropFunction: "Drop Function",
confirmDropViewTitle: "Drop View",
confirmDropViewMessage: 'Are you sure you want to drop view "{name}"?',
confirmDropProcedureTitle: "Drop Procedure",
confirmDropProcedureMessage: 'Are you sure you want to drop procedure "{name}"?',
confirmDropFunctionTitle: "Drop Function",
confirmDropFunctionMessage: 'Are you sure you want to drop function "{name}"?',
dropViewSuccess: 'View "{name}" dropped',
dropProcedureSuccess: 'Procedure "{name}" dropped',
dropFunctionSuccess: 'Function "{name}" dropped',
emptyTableSuccess: 'All data deleted from "{name}"',
truncateTableSuccess: 'Table "{name}" truncated',
duplicateStructureSuccess: 'Table structure duplicated as "{name}"',
@@ -573,6 +586,10 @@ export default {
indexes: "Indexes",
foreignKeys: "Foreign Keys",
triggers: "Triggers",
tables: "Tables",
views: "Views",
procedures: "Procedures",
functions: "Functions",
objectBrowser: "Browse in Object Browser ({count})",
},
objects: {
+17
View File
@@ -545,6 +545,19 @@ export default {
confirmTruncateTableTitle: "Truncar tabla",
confirmTruncateTableMessage: '¿Estás seguro de que deseas truncar "{name}"? Esto eliminará todas las filas.',
dropTableSuccess: 'Tabla "{name}" eliminada',
viewSource: "Ver código fuente",
dropView: "Eliminar vista",
dropProcedure: "Eliminar procedimiento",
dropFunction: "Eliminar función",
confirmDropViewTitle: "Eliminar vista",
confirmDropViewMessage: '¿Seguro que deseas eliminar la vista "{name}"?',
confirmDropProcedureTitle: "Eliminar procedimiento",
confirmDropProcedureMessage: '¿Seguro que deseas eliminar el procedimiento "{name}"?',
confirmDropFunctionTitle: "Eliminar función",
confirmDropFunctionMessage: '¿Seguro que deseas eliminar la función "{name}"?',
dropViewSuccess: 'Vista "{name}" eliminada',
dropProcedureSuccess: 'Procedimiento "{name}" eliminado',
dropFunctionSuccess: 'Función "{name}" eliminada',
emptyTableSuccess: 'Todos los datos eliminados de "{name}"',
truncateTableSuccess: 'Tabla "{name}" truncada',
duplicateStructureSuccess: 'Estructura de tabla duplicada como "{name}"',
@@ -574,6 +587,10 @@ export default {
indexes: "Índices",
foreignKeys: "Claves foráneas",
triggers: "Disparadores",
tables: "Tablas",
views: "Vistas",
procedures: "Procedimientos",
functions: "Funciones",
objectBrowser: "Explorar en el navegador de objetos ({count})",
},
objects: {
+17
View File
@@ -533,6 +533,19 @@ export default {
confirmTruncateTableTitle: "截断表",
confirmTruncateTableMessage: "确定要截断「{name}」吗?这将删除所有行。",
dropTableSuccess: "表「{name}」已删除",
viewSource: "查看源码",
dropView: "删除视图",
dropProcedure: "删除存储过程",
dropFunction: "删除函数",
confirmDropViewTitle: "删除视图",
confirmDropViewMessage: "确定要删除视图「{name}」吗?",
confirmDropProcedureTitle: "删除存储过程",
confirmDropProcedureMessage: "确定要删除存储过程「{name}」吗?",
confirmDropFunctionTitle: "删除函数",
confirmDropFunctionMessage: "确定要删除函数「{name}」吗?",
dropViewSuccess: "视图「{name}」已删除",
dropProcedureSuccess: "存储过程「{name}」已删除",
dropFunctionSuccess: "函数「{name}」已删除",
emptyTableSuccess: "已清空「{name}」的所有数据",
truncateTableSuccess: "表「{name}」已截断",
duplicateStructureSuccess: "已复制表结构为「{name}」",
@@ -560,6 +573,10 @@ export default {
indexes: "索引",
foreignKeys: "外键",
triggers: "触发器",
tables: "表",
views: "视图",
procedures: "存储过程",
functions: "函数",
objectBrowser: "在对象浏览器中查看 ({count})",
},
objects: {
+2
View File
@@ -5,6 +5,8 @@ const leafTypes: Set<TreeNodeType> = new Set([
"index",
"fkey",
"trigger",
"procedure",
"function",
"object-browser",
"redis-db",
"mongo-collection",
+84 -1
View File
@@ -1,4 +1,4 @@
import type { TableInfo, TreeNode } from "@/types/database";
import type { ObjectInfo, TableInfo, TreeNode, TreeNodeType } from "@/types/database";
export function buildTableTreeNodes({
nodeId,
@@ -25,6 +25,89 @@ export function buildTableTreeNodes({
}));
}
function normalizeObjectType(type: string): "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" {
const v = type.toUpperCase();
if (v.includes("VIEW")) return "VIEW";
if (v.includes("PROC")) return "PROCEDURE";
if (v.includes("FUNC")) return "FUNCTION";
return "TABLE";
}
const groupDefs: Array<{
key: string;
label: string;
objectType: string;
nodeType: TreeNodeType;
childType: TreeNodeType;
}> = [
{ key: "__tables", label: "tree.tables", objectType: "TABLE", nodeType: "group-tables", childType: "table" },
{ key: "__views", label: "tree.views", objectType: "VIEW", nodeType: "group-views", childType: "view" },
{
key: "__procedures",
label: "tree.procedures",
objectType: "PROCEDURE",
nodeType: "group-procedures",
childType: "procedure",
},
{
key: "__functions",
label: "tree.functions",
objectType: "FUNCTION",
nodeType: "group-functions",
childType: "function",
},
];
export function buildGroupedObjectTreeNodes({
nodeId,
connectionId,
database,
schema,
objects,
}: {
nodeId: string;
connectionId: string;
database: string;
schema?: string;
objects: ObjectInfo[];
}): TreeNode[] {
const buckets = new Map<string, ObjectInfo[]>();
for (const obj of objects) {
const t = normalizeObjectType(obj.object_type);
const arr = buckets.get(t) ?? [];
arr.push(obj);
buckets.set(t, arr);
}
const groups: TreeNode[] = [];
for (const def of groupDefs) {
const items = buckets.get(def.objectType);
if (!items?.length) continue;
const isExpandable = def.childType === "table" || def.childType === "view";
groups.push({
id: `${nodeId}:${def.key}`,
label: def.label,
type: def.nodeType,
connectionId,
database,
schema,
objectCount: items.length,
isExpanded: false,
children: items.map((obj) => ({
id: `${nodeId}:${obj.name}`,
label: obj.name,
type: def.childType,
connectionId,
database,
schema,
isExpanded: false,
children: isExpandable ? [] : undefined,
})),
});
}
return groups;
}
export function expandCachedObjectBrowserNodes(nodes: TreeNode[]): TreeNode[] {
return nodes.flatMap((node) => {
if (node.type === "object-browser") return node.hiddenChildren ?? [];
+17 -10
View File
@@ -23,7 +23,7 @@ import { isTauriRuntime } from "@/lib/tauriRuntime";
import { isSchemaAware, TREE_SCHEMA_TYPES } from "@/lib/databaseCapabilities";
import { buildDatabaseTreeNodes } from "@/lib/databaseTree";
import { buildSqlServerDatabaseTreeNodes, SQLSERVER_DEFAULT_SCHEMA } from "@/lib/sqlServerTree";
import { buildTableTreeNodes, expandCachedObjectBrowserNodes } from "@/lib/tableTree";
import { buildGroupedObjectTreeNodes, buildTableTreeNodes, expandCachedObjectBrowserNodes } from "@/lib/tableTree";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
@@ -685,20 +685,20 @@ export const useConnectionStore = defineStore("connection", () => {
try {
await ensureConnected(connectionId);
if (useCachedChildren(node, options)) return;
const cacheKey = schemaCacheKey(connectionId, database, schema || "", "tables");
const cacheKey = schemaCacheKey(connectionId, database, schema || "", "objects");
if (!options?.force && (await loadPersistedTreeChildren(node, cacheKey))) return;
const querySchema = schema || database;
const tables = await api.listTables(connectionId, database, querySchema);
const config = getConfig(connectionId);
const effectiveSchema = schema || (config?.db_type && isSchemaAware(config.db_type) ? database : undefined);
const children = buildTableTreeNodes({
nodeId,
connectionId,
database,
schema: effectiveSchema,
tables,
});
let children: TreeNode[];
try {
const objects = await api.listObjects(connectionId, database, querySchema);
children = buildGroupedObjectTreeNodes({ nodeId, connectionId, database, schema: effectiveSchema, objects });
} catch {
const tables = await api.listTables(connectionId, database, querySchema);
children = buildTableTreeNodes({ nodeId, connectionId, database, schema: effectiveSchema, tables });
}
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
@@ -919,6 +919,13 @@ export const useConnectionStore = defineStore("connection", () => {
await loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema);
} else if (node.type === "group-triggers" && node.connectionId && node.database && node.tableName) {
await loadTriggers(node.connectionId, node.database, node.tableName, node.schema);
} else if (
node.type === "group-tables" ||
node.type === "group-views" ||
node.type === "group-procedures" ||
node.type === "group-functions"
) {
node.isExpanded = true;
}
}
+6
View File
@@ -167,10 +167,16 @@ export type TreeNodeType =
| "schema"
| "table"
| "view"
| "procedure"
| "function"
| "group-columns"
| "group-indexes"
| "group-fkeys"
| "group-triggers"
| "group-tables"
| "group-views"
| "group-procedures"
| "group-functions"
| "object-browser"
| "saved-sql-root"
| "saved-sql-folder"