feat(sidebar): extend table grouping to all object containers

This commit is contained in:
ReadMe.md authored and GitHub committed 2026-09-22 15:52:04 +08:00
1 parent e1a512fd59
commit 591e8f4909
20 files changed
+607 -169

No files matched your search

@@ -23,8 +23,10 @@ function confirmCreate() {
if (!scope || !name) return;
const groupId = connectionStore.createTableVGroup(scope, name, tableVGroupDialogParentGroupId.value);
if (groupId) {
// scope 即右键的源行节点,其 type 就是这批待移入行的类别(多选保证同类型)。
const rowType = scope.type;
for (const tableName of tableVGroupDialogTableNames.value) {
connectionStore.moveTableToVGroup(scope, tableName, groupId);
connectionStore.moveTableToVGroup(scope, tableName, groupId, rowType);
}
emit("created", tableVGroupNodeId(groupId));
}
@@ -85,7 +85,7 @@ import { canTreeNodePin, canTreeNodeShowExpander } from "@/lib/sidebar/sidebarTr
import { sidebarConnectionVisibleFilterMenu } from "@/lib/sidebar/sidebarVisibleFilterMenu";
import { supportsSidebarObjectNameFilter } from "@/lib/sidebar/sidebarObjectNameFilter";
import { connectionGroupDestinationRows } from "@/lib/sidebar/sidebarLayout";
import { hasTableVGroupEntries, selectedTableVGroupMoveTargets, tableVGroupDestinationRows, tableVGroupPathForTable, tableVGroupsEnabled } from "@/lib/table/tableVGroup";
import { hasTableVGroupEntries, isTableVGroupContainerNode, isTableVGroupGroupableRowType, resolveTableVGroupScopeFromNode, selectedTableVGroupMoveTargets, tableVGroupDestinationRows, tableVGroupPathForTable, tableVGroupScopeKey, tableVGroupsEnabled } from "@/lib/table/tableVGroup";
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
import { requestObjectBrowserSearchFocus } from "@/lib/tabs/objectBrowserSearchFocus";
@@ -5958,6 +5958,11 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
if (currentDatabaseType() === "hbase" && node.type === "table") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
// HBase 表行走本特化分支(先于 buildObjectSidebarMenu 的统一注入短路),分组移动入口需在此补齐。
if (isTableVGroupGroupableRowType(node.type) && !!tableVGroupScopeKey(resolveTableVGroupScopeFromNode(connectionStore.treeNodes, node))) {
const vgroupMoveItems = buildTableVGroupMoveMenuItems(node);
if (vgroupMoveItems.length) items.push({ label: t("tableVGroup.moveToGroup"), icon: FolderInput, children: vgroupMoveItems });
}
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: openDataImmediately, icon: TableProperties });
items.push({
@@ -6150,6 +6155,15 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
const { node, items, deleteMenuLabel, deleteMenuAction, truncateMenuLabel, truncateMenuAction, emptyMenuLabel, emptyMenuAction, batchAutoIncrementCount, autoIncrementMenuLabel, autoIncrementMenuAction } = context;
// 虚拟分组移动入口:对所有可分组行类型统一注入(含 procedure/function/trigger/
// sequence/event/synonym/job/package/type 等专属分支——它们各自 return true,
// 若只在 table/view/mv 分支注入,这些行将没有任何分组入口)。表行保持原行为
// (simple 模式也能经「移动到新分组」建组);其余行须已处于同类别分组容器内,
// 否则解析不出 scope,菜单只会静默失败。
if (isTableVGroupGroupableRowType(node.type) && (node.type === "table" || !!tableVGroupScopeKey(resolveTableVGroupScopeFromNode(connectionStore.treeNodes, node)))) {
const vgroupMoveItems = buildTableVGroupMoveMenuItems(node);
if (vgroupMoveItems.length) items.push({ label: t("tableVGroup.moveToGroup"), icon: FolderInput, children: vgroupMoveItems });
}
// 6. Table / View / Materialized View
if (node.type === "table" || node.type === "view" || node.type === "materialized_view") {
if (currentDatabaseType() === "victoriametrics" && node.type === "table") {
@@ -6169,10 +6183,6 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
}
const destructiveActions: ContextMenuItem[] = [];
items.push(copyNameMenuItem());
if (node.type === "table") {
const vgroupMoveItems = buildTableVGroupMoveMenuItems(node);
if (vgroupMoveItems.length) items.push({ label: t("tableVGroup.moveToGroup"), icon: FolderInput, children: vgroupMoveItems });
}
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (node.type === "table" && supportsAiAssistantContext(currentDatabaseType())) {
items.push(addToAiMenuItem(node));
@@ -6590,20 +6600,21 @@ function buildTableVGroupMoveMenuItems(node: TreeNode): ContextMenuItem[] {
const layout = connectionStore.tableVGroupLayoutFor(node);
const targets = selectedTableVGroupMoveTargets(node, selectedTreeNodesInVisibleOrder());
const targetNames = targets.map((target) => target.label);
const targetsInGroup = (groupId: string) => targetNames.every((name) => tableVGroupPathForTable(layout, name).includes(groupId));
const targetRowType = targets[0]?.type;
const targetsInGroup = (groupId: string) => targetNames.every((name) => tableVGroupPathForTable(layout, name, targetRowType).includes(groupId));
const items: ContextMenuItem[] = tableVGroupDestinationRows(layout).map((row) => ({
label: row.name,
title: row.path.join(" / "),
disabled: targetNames.length > 0 && targetsInGroup(row.id),
action: () => {
for (const name of targetNames) connectionStore.moveTableToVGroup(node, name, row.id);
for (const name of targetNames) connectionStore.moveTableToVGroup(node, name, row.id, targetRowType);
},
}));
if (targetNames.some((name) => tableVGroupPathForTable(layout, name).length > 0)) {
if (targetNames.some((name) => tableVGroupPathForTable(layout, name, targetRowType).length > 0)) {
items.push({
label: t("tableVGroup.removeFromGroup"),
action: () => {
for (const name of targetNames) connectionStore.moveTableToVGroup(node, name, null);
for (const name of targetNames) connectionStore.moveTableToVGroup(node, name, null, targetRowType);
},
});
}
@@ -6623,7 +6634,7 @@ function buildTableVGroupMoveMenuItems(node: TreeNode): ContextMenuItem[] {
}
function appendTableVGroupContainerItems(node: TreeNode, items: ContextMenuItem[]) {
if (node.type !== "database" && node.type !== "schema" && node.type !== "linked-server-schema" && node.type !== "group-tables") return;
if (!isTableVGroupContainerNode(node)) return;
const layout = connectionStore.tableVGroupLayoutFor(node);
if (!hasTableVGroupEntries(layout)) return;
const enabled = tableVGroupsEnabled(layout);
@@ -74,7 +74,7 @@ import { formatSidebarObjectStorage } from "@/lib/sidebar/sidebarDatabaseStorage
import { effectiveRedisDatabaseIndex } from "@/lib/redis/redisDatabaseIndex";
import { dataTabOpenModeFromTreeClick } from "@/lib/sidebar/dataTabOpenPolicy";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { selectedTableVGroupMoveTargets, tableVGroupIdFromNodeId } from "@/lib/table/tableVGroup";
import { isTableVGroupGroupableRowType, selectedTableVGroupMoveTargets, tableVGroupIdFromNodeId } from "@/lib/table/tableVGroup";
import { findTreeNodeById } from "@/lib/sql/newQueryContext";
import { resolveTableVGroupDropTarget, setTableVGroupDropTargetNodeId, tableVGroupDropTargetNodeId } from "@/lib/sidebar/sidebarTableVGroupDrag";
import { connectionDisplayUrlScheme } from "@/lib/connection/connectionPresentation";
@@ -1375,9 +1375,10 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
function startTableReferenceDrag(payload: QueryEditorTableReferencePayload) {
draggingTableReferencePayload = payload;
// 分组成员名单只收真实表:视图/物化视图投影不识别,入组会产生隐形脏数据。
// 分组成员名单按可分组行类型收集(表/视图/物化视图/过程/函数/触发器/序列等);
// 无匹配容器的行由 scope 解析兜底拒绝,不会产生隐形脏数据。
vgroupDragTableNames = selectedTableVGroupMoveTargets(activeNode.value, selectedTreeNodesInVisibleOrder())
.filter((node) => node.type === "table")
.filter((node) => isTableVGroupGroupableRowType(node.type))
.map((node) => node.label);
setActiveTableReferencePayload(payload);
document.getSelection()?.removeAllRanges();
@@ -1402,7 +1403,8 @@ let vgroupDragTableNames: string[] = [];
function tableVGroupDropTargetFor(payload: QueryEditorTableReferencePayload, event: MouseEvent) {
if (!vgroupDragTableNames.length) return null;
return resolveTableVGroupDropTarget(event.clientX, event.clientY, connectionStore.treeNodes, payload);
// 拖拽源是树行(多选同类型),落点解析按行类别过滤跨类别容器。
return resolveTableVGroupDropTarget(event.clientX, event.clientY, connectionStore.treeNodes, { ...payload, objectType: activeNode.value.type });
}
function onTableReferenceMouseMove(event: MouseEvent) {
@@ -1431,7 +1433,7 @@ function onTableReferenceMouseUp(event: MouseEvent) {
suppressNextTableReferenceClick = true;
const dropTarget = tableVGroupDropTargetFor(payload, event);
if (dropTarget) {
for (const tableName of vgroupDragTableNames) connectionStore.moveTableToVGroup(dropTarget.node, tableName, dropTarget.groupId);
for (const tableName of vgroupDragTableNames) connectionStore.moveTableToVGroup(dropTarget.node, tableName, dropTarget.groupId, activeNode.value.type);
} else {
const target = document.elementFromPoint(event.clientX, event.clientY);
if (target instanceof Element && target.closest(`[data-query-editor-root], ${AI_ASSISTANT_TABLE_DROP_ROOT_SELECTOR}`)) {
@@ -24,7 +24,7 @@ export const deleteConnectionsWithGroup = ref(false);
export const showTableVGroupDialog = ref(false);
export const tableVGroupName = ref("");
/** Scope snapshot + creation payload for the table vgroup naming dialog. */
export const tableVGroupDialogScope = shallowRef<TableVGroupScope | null>(null);
export const tableVGroupDialogScope = shallowRef<TreeNode | null>(null);
export const tableVGroupDialogParentGroupId = ref<string | null>(null);
export const tableVGroupDialogTableNames = ref<string[]>([]);
export const showTableVGroupDeleteConfirm = ref(false);