feat(sidebar): add nested virtual table groups with drag and batch move
This commit is contained in:
1 parent
0db9756a57
commit
1f43d184aa
31 files changed
+1714
-20
No files matched your search
@@ -64,6 +64,7 @@ import SidebarLocateButton from "./SidebarLocateButton.vue";
|
||||
import SidebarRegexToggleButton from "./SidebarRegexToggleButton.vue";
|
||||
import SidebarTreeRuntimeHost from "./SidebarTreeRuntimeHost.vue";
|
||||
import SidebarTreeItemDialogs from "./SidebarTreeItemDialogs.vue";
|
||||
import SidebarTableVGroupDialog from "./SidebarTableVGroupDialog.vue";
|
||||
import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog.vue";
|
||||
import ExtensionDetailsDialog from "@/components/objects/ExtensionDetailsDialog.vue";
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
@@ -1483,6 +1484,12 @@ function topOcclusionHeightForSidebarNode(nodeId: string): number {
|
||||
return SIDEBAR_TREE_ROW_HEIGHT;
|
||||
}
|
||||
|
||||
/** Select and reveal a freshly created table group. */
|
||||
async function focusCreatedTableVGroup(groupId: string) {
|
||||
store.selectedTreeNodeId = groupId;
|
||||
await scrollToSidebarNode(groupId);
|
||||
}
|
||||
|
||||
async function scrollToSidebarNode(nodeId: string, options?: { align?: SidebarNodeScrollAlign }) {
|
||||
await nextTick();
|
||||
|
||||
@@ -1655,6 +1662,14 @@ async function locateTabInSidebar(tab: QueryTab | undefined | null, align: Sideb
|
||||
}
|
||||
}
|
||||
|
||||
// 表分组行同样是投影出的合成节点(不登记已加载子节点,上面的守卫会跳过),
|
||||
// 折叠状态存在布局里,必须经布局 op 展开,否则下次投影又把它折叠回去。
|
||||
for (const node of nodePath) {
|
||||
if (node.type !== "table-vgroup" || !node.vgroupId) continue;
|
||||
const group = store.tableVGroupLayoutFor(node)?.groups.find((current) => current.id === node.vgroupId);
|
||||
if (group?.collapsed) store.toggleTableVGroupCollapsed(node, node.vgroupId);
|
||||
}
|
||||
|
||||
// Connection groups never register loaded tree children, so the guard above
|
||||
// skips them and a collapsed group keeps the target out of the visible flat
|
||||
// tree. Reopen them through the layout op so the expansion is persisted and
|
||||
@@ -2855,6 +2870,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
|
||||
</template>
|
||||
</SidebarDangerConfirmDialog>
|
||||
<SidebarTreeItemDialogs v-if="sidebarTreeItemDialogController" :key="sidebarTreeItemDialogController.node?.id" :controller="sidebarTreeItemDialogController" @closed="sidebarTreeItemDialogController = null" />
|
||||
<SidebarTableVGroupDialog @created="focusCreatedTableVGroup" />
|
||||
<InstallExtensionDialog v-if="sidebarInstallExtensionTarget" ref="sidebarInstallExtensionDialogRef" :node="sidebarInstallExtensionTarget" @close="refreshSidebarActionTarget" @changed="refreshSidebarActionTarget" />
|
||||
<ExtensionDetailsDialog v-if="sidebarExtensionDetailsTarget" ref="sidebarExtensionDetailsDialogRef" :node="sidebarExtensionDetailsTarget" />
|
||||
<div v-if="store.treeNodes.length === 0" class="px-3 py-8 text-center text-muted-foreground text-xs">
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { tableVGroupNodeId } from "@/lib/table/tableVGroup";
|
||||
import { showTableVGroupDialog, tableVGroupName, tableVGroupDialogScope, tableVGroupDialogParentGroupId, tableVGroupDialogTableNames, showTableVGroupDeleteConfirm, tableVGroupDeleteTarget } from "./sidebarTreeDialogState";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const emit = defineEmits<{ created: [groupId: string] }>();
|
||||
|
||||
const deleteConfirmMessage = computed(() => t("tableVGroup.deleteGroupConfirmMessage", { name: tableVGroupDeleteTarget.value?.name ?? "" }));
|
||||
|
||||
function confirmCreate() {
|
||||
const scope = tableVGroupDialogScope.value;
|
||||
const name = tableVGroupName.value.trim();
|
||||
if (!scope || !name) return;
|
||||
const groupId = connectionStore.createTableVGroup(scope, name, tableVGroupDialogParentGroupId.value);
|
||||
if (groupId) {
|
||||
for (const tableName of tableVGroupDialogTableNames.value) {
|
||||
connectionStore.moveTableToVGroup(scope, tableName, groupId);
|
||||
}
|
||||
emit("created", tableVGroupNodeId(groupId));
|
||||
}
|
||||
showTableVGroupDialog.value = false;
|
||||
tableVGroupName.value = "";
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
const target = tableVGroupDeleteTarget.value;
|
||||
showTableVGroupDeleteConfirm.value = false;
|
||||
tableVGroupDeleteTarget.value = null;
|
||||
if (!target) return;
|
||||
connectionStore.deleteTableVGroups(target.scope, [target.groupId]);
|
||||
toast(t("tableVGroup.groupDeleted"), 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showTableVGroupDialog">
|
||||
<DialogContent class="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ tableVGroupDialogTableNames.length ? t("tableVGroup.moveToNewGroup") : t("tableVGroup.newSubgroup") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input v-model="tableVGroupName" :placeholder="t('connectionGroup.groupNamePlaceholder')" @keydown.enter.prevent="confirmCreate" />
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showTableVGroupDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!tableVGroupName.trim()" @click="confirmCreate">{{ t("connectionGroup.createGroup") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showTableVGroupDeleteConfirm">
|
||||
<DialogContent class="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("tableVGroup.deleteGroupConfirmTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p class="text-sm text-muted-foreground">{{ deleteConfirmMessage }}</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showTableVGroupDeleteConfirm = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="destructive" @click="confirmDelete">{{ t("tableVGroup.deleteGroup") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -84,6 +84,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 { objectTypesForGroupNode } from "@/lib/table/tableTree";
|
||||
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
||||
import { requestObjectBrowserSearchFocus } from "@/lib/tabs/objectBrowserSearchFocus";
|
||||
@@ -219,6 +220,13 @@ import {
|
||||
sidebarDangerRunningCancel,
|
||||
sidebarFormTarget,
|
||||
showDeleteConfirm,
|
||||
showTableVGroupDialog,
|
||||
showTableVGroupDeleteConfirm,
|
||||
tableVGroupDeleteTarget,
|
||||
tableVGroupName,
|
||||
tableVGroupDialogScope,
|
||||
tableVGroupDialogParentGroupId,
|
||||
tableVGroupDialogTableNames,
|
||||
showDropTableConfirm,
|
||||
showDropTableChildObjectConfirm,
|
||||
showBatchDropConfirm,
|
||||
@@ -819,6 +827,13 @@ async function toggle(requestId = beginNavigationRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "table-vgroup") {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
if (node.vgroupId) connectionStore.toggleTableVGroupCollapsed(node, node.vgroupId);
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "type" && customTypeCapabilities(currentDatabaseType()).details && node.children !== undefined) {
|
||||
node.isExpanded = node.children.length > 0 ? !node.isExpanded : false;
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
@@ -1402,6 +1417,10 @@ function requestRenameSelectedNode(): boolean {
|
||||
startRenameGroup();
|
||||
return true;
|
||||
}
|
||||
if (activeNode.value.type === "table-vgroup" && activeNode.value.vgroupId) {
|
||||
emit("request-group-rename", activeNode.value.id);
|
||||
return true;
|
||||
}
|
||||
if (activeNode.value.type === "saved-sql-file" && activeNode.value.savedSqlId) {
|
||||
emit("request-saved-sql-rename", activeNode.value.id);
|
||||
return true;
|
||||
@@ -1463,6 +1482,10 @@ function requestDeleteSelectedNode(): boolean {
|
||||
deleteConnectionGroup();
|
||||
return true;
|
||||
}
|
||||
if (activeNode.value.type === "table-vgroup") {
|
||||
requestTableVGroupDelete(activeNode.value);
|
||||
return true;
|
||||
}
|
||||
if (canDropDatabase.value) {
|
||||
dropDatabase();
|
||||
return true;
|
||||
@@ -5707,6 +5730,7 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
|
||||
|
||||
function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
const { node, items } = context;
|
||||
appendTableVGroupContainerItems(node, items);
|
||||
// 4. Database / Schema
|
||||
if (node.type === "database" || node.type === "schema") {
|
||||
if (isXuguSyntheticTreeNode(currentDatabaseType(), node.type, node.schema)) {
|
||||
@@ -6104,6 +6128,10 @@ 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));
|
||||
@@ -6435,6 +6463,7 @@ function treeTableClipboardMenuItems(node: TreeNode): ContextMenuItem[] {
|
||||
|
||||
function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
const { node, items } = context;
|
||||
appendTableVGroupContainerItems(node, items);
|
||||
// 9. Group Labels (group-columns, group-tables, etc.)
|
||||
if (isGroupLabel(node)) {
|
||||
const mysqlObjectTemplate = node.connectionId ? mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null;
|
||||
@@ -6506,7 +6535,89 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
||||
return false;
|
||||
}
|
||||
|
||||
const sidebarMenuFactories: readonly SidebarMenuFactory[] = [buildConnectionSidebarMenu, buildDatabaseSidebarMenu, buildSpecialSidebarMenu, buildObjectSidebarMenu, buildObjectGroupSidebarMenu];
|
||||
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 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);
|
||||
},
|
||||
}));
|
||||
if (targetNames.some((name) => tableVGroupPathForTable(layout, name).length > 0)) {
|
||||
items.push({
|
||||
label: t("tableVGroup.removeFromGroup"),
|
||||
action: () => {
|
||||
for (const name of targetNames) connectionStore.moveTableToVGroup(node, name, null);
|
||||
},
|
||||
});
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: t("tableVGroup.moveToNewGroup"),
|
||||
action: () => {
|
||||
tableVGroupDialogScope.value = node;
|
||||
tableVGroupDialogParentGroupId.value = null;
|
||||
tableVGroupDialogTableNames.value = targetNames;
|
||||
tableVGroupName.value = "";
|
||||
showTableVGroupDialog.value = true;
|
||||
},
|
||||
icon: FolderPlus,
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function appendTableVGroupContainerItems(node: TreeNode, items: ContextMenuItem[]) {
|
||||
if (node.type !== "database" && node.type !== "schema" && node.type !== "linked-server-schema" && node.type !== "group-tables") return;
|
||||
const layout = connectionStore.tableVGroupLayoutFor(node);
|
||||
if (!hasTableVGroupEntries(layout)) return;
|
||||
const enabled = tableVGroupsEnabled(layout);
|
||||
items.push({
|
||||
label: enabled ? t("tableVGroup.disableGroups") : t("tableVGroup.enableGroups"),
|
||||
action: () => connectionStore.setTableVGroupsEnabled(node, !enabled),
|
||||
icon: FolderInput,
|
||||
});
|
||||
items.push({ label: "", separator: true });
|
||||
}
|
||||
|
||||
function buildTableVGroupSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
const { node, items } = context;
|
||||
if (node.type !== "table-vgroup" || !node.vgroupId) return false;
|
||||
const groupId = node.vgroupId;
|
||||
items.push({
|
||||
label: t("tableVGroup.newSubgroup"),
|
||||
action: () => {
|
||||
tableVGroupDialogScope.value = node;
|
||||
tableVGroupDialogParentGroupId.value = groupId;
|
||||
tableVGroupDialogTableNames.value = [];
|
||||
tableVGroupName.value = "";
|
||||
showTableVGroupDialog.value = true;
|
||||
},
|
||||
icon: FolderPlus,
|
||||
});
|
||||
items.push({ label: t("connectionGroup.renameGroup"), action: () => emit("request-group-rename", node.id), icon: Pencil, shortcut: shortcutRename });
|
||||
items.push({
|
||||
label: t("tableVGroup.deleteGroup"),
|
||||
action: () => requestTableVGroupDelete(node),
|
||||
icon: Trash2,
|
||||
variant: "destructive" as const,
|
||||
shortcut: shortcutDelete,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Open the shared confirmation before removing a group (its tables stay). */
|
||||
function requestTableVGroupDelete(node: TreeNode) {
|
||||
if (node.type !== "table-vgroup" || !node.vgroupId) return;
|
||||
tableVGroupDeleteTarget.value = { scope: node, groupId: node.vgroupId, name: node.label };
|
||||
showTableVGroupDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
const sidebarMenuFactories: readonly SidebarMenuFactory[] = [buildConnectionSidebarMenu, buildDatabaseSidebarMenu, buildSpecialSidebarMenu, buildTableVGroupSidebarMenu, buildObjectSidebarMenu, buildObjectGroupSidebarMenu];
|
||||
|
||||
function treeItemMenuItems(): ContextMenuItem[] {
|
||||
const node = activeNode.value;
|
||||
|
||||
@@ -74,6 +74,9 @@ 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 { findTreeNodeById } from "@/lib/sql/newQueryContext";
|
||||
import { resolveTableVGroupDropTarget, setTableVGroupDropTargetNodeId, tableVGroupDropTargetNodeId } from "@/lib/sidebar/sidebarTableVGroupDrag";
|
||||
import { connectionDisplayUrlScheme } from "@/lib/connection/connectionPresentation";
|
||||
import { isFocusSearchShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { encodeSpannerResourcePath } from "@/lib/connection/spannerResourcePath";
|
||||
@@ -248,6 +251,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
return null;
|
||||
case "connection-group":
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-amber-500" };
|
||||
case "table-vgroup":
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-emerald-500" };
|
||||
case "database":
|
||||
return { icon: Database, colorClass: "text-yellow-500" };
|
||||
case "tablespace":
|
||||
@@ -1075,7 +1080,11 @@ const renameInput = ref("");
|
||||
|
||||
const renameInputRef = ref<HTMLInputElement>();
|
||||
|
||||
/** Snapshot of the row being renamed: reprojecting the tree mid-rename recycles activeNode. */
|
||||
const renameTargetNode = shallowRef<TreeNode | null>(null);
|
||||
|
||||
function startRenameGroup() {
|
||||
renameTargetNode.value = activeNode.value;
|
||||
renameInput.value = activeNode.value.label;
|
||||
isRenamingGroup.value = true;
|
||||
emit("rename-started");
|
||||
@@ -1105,6 +1114,7 @@ watch(
|
||||
if (activeNode.value.type === "connection-group") startRenameGroup();
|
||||
else if (activeNode.value.type === "saved-sql-file") startRenameSavedSql();
|
||||
else if (activeNode.value.type === "connection") startRenameConnection();
|
||||
else if (activeNode.value.type === "table-vgroup") startRenameGroup();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -1126,11 +1136,17 @@ function finishRenameGroup() {
|
||||
// groups (issue #681).
|
||||
if (!isRenamingGroup.value) return;
|
||||
isRenamingGroup.value = false;
|
||||
const target = renameTargetNode.value ?? activeNode.value;
|
||||
renameTargetNode.value = null;
|
||||
const trimmed = renameInput.value.trim();
|
||||
// An empty name cancels the rename and keeps the group as-is — never delete
|
||||
// here. Deleting a group is done explicitly via the context menu (issue #681).
|
||||
if (!trimmed || trimmed === activeNode.value.label) return;
|
||||
connectionStore.renameConnectionGroup(activeNode.value.id, trimmed);
|
||||
if (!trimmed || trimmed === target.label) return;
|
||||
if (target.type === "table-vgroup" && target.vgroupId) {
|
||||
connectionStore.renameTableVGroup(target, target.vgroupId, trimmed);
|
||||
return;
|
||||
}
|
||||
connectionStore.renameConnectionGroup(target.id, trimmed);
|
||||
}
|
||||
|
||||
async function finishRenameSavedSql() {
|
||||
@@ -1193,6 +1209,19 @@ const {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragState.draggedType === "table-vgroup") {
|
||||
const draggedGroupId = tableVGroupIdFromNodeId(draggedId);
|
||||
const targetGroupId = tableVGroupIdFromNodeId(targetId);
|
||||
// 落点回调是模块级单例,activeNode 未必是同容器的落点行,须用 targetId 现查。
|
||||
const targetNode = findTreeNodeById(connectionStore.treeNodes, targetId);
|
||||
if (draggedGroupId && targetGroupId && targetNode) connectionStore.reorderTableVGroupEntry(targetNode, draggedGroupId, targetGroupId, position);
|
||||
return;
|
||||
}
|
||||
|
||||
// 分组行只在拖动分组自身时才是合法落点:否则 targetId 不在侧边栏布局里,
|
||||
// reorderSidebarEntries 找不到目标会把连接追加到根列表末尾。
|
||||
if (tableVGroupIdFromNodeId(targetId)) return;
|
||||
|
||||
// If the grabbed row is part of a multi-selection, move all selected rows
|
||||
// together; otherwise just the grabbed one (issue #681).
|
||||
const selected = connectionStore.selectedTreeNodeIds;
|
||||
@@ -1202,7 +1231,7 @@ const {
|
||||
|
||||
const canReorderTreeNode = computed(() => {
|
||||
if (props.reorderDisabled) return false;
|
||||
return activeNode.value.type === "connection" || activeNode.value.type === "connection-group";
|
||||
return activeNode.value.type === "connection" || activeNode.value.type === "connection-group" || activeNode.value.type === "table-vgroup";
|
||||
});
|
||||
|
||||
function isPinnedOrderDrag(): boolean {
|
||||
@@ -1211,7 +1240,7 @@ function isPinnedOrderDrag(): boolean {
|
||||
|
||||
const dragVisual = computed(() => {
|
||||
const targetId = isPinnedOrderDrag() ? pinnedSortKey() : activeNode.value.id;
|
||||
const isDropTarget = isPinnedOrderDrag() ? connectionStore.isPinnedTreeNodeReorderTarget(pinnedSortKey()) : activeNode.value.type === "connection" || activeNode.value.type === "connection-group";
|
||||
const isDropTarget = isPinnedOrderDrag() ? connectionStore.isPinnedTreeNodeReorderTarget(pinnedSortKey()) : activeNode.value.type === "connection" || activeNode.value.type === "connection-group" || activeNode.value.type === "table-vgroup";
|
||||
|
||||
return {
|
||||
isDropTarget,
|
||||
@@ -1346,6 +1375,10 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
|
||||
function startTableReferenceDrag(payload: QueryEditorTableReferencePayload) {
|
||||
draggingTableReferencePayload = payload;
|
||||
// 分组成员名单只收真实表:视图/物化视图投影不识别,入组会产生隐形脏数据。
|
||||
vgroupDragTableNames = selectedTableVGroupMoveTargets(activeNode.value, selectedTreeNodesInVisibleOrder())
|
||||
.filter((node) => node.type === "table")
|
||||
.map((node) => node.label);
|
||||
setActiveTableReferencePayload(payload);
|
||||
document.getSelection()?.removeAllRanges();
|
||||
referenceDragFeedback = beginTableReferenceDragFeedback(tableReferenceDragLabel(payload));
|
||||
@@ -1355,6 +1388,8 @@ function finishTableReferenceDrag() {
|
||||
clearActiveTableReferencePayload(draggingTableReferencePayload);
|
||||
pendingTableReferenceDrag = null;
|
||||
draggingTableReferencePayload = null;
|
||||
vgroupDragTableNames = [];
|
||||
setTableVGroupDropTargetNodeId(null);
|
||||
referenceDragFeedback?.end();
|
||||
referenceDragFeedback = null;
|
||||
window.dispatchEvent(createTableReferenceDragEndEvent());
|
||||
@@ -1362,6 +1397,14 @@ function finishTableReferenceDrag() {
|
||||
document.removeEventListener("mouseup", onTableReferenceMouseUp, true);
|
||||
}
|
||||
|
||||
/** 本次拖拽要移动进分组的表名(拖拽开始时按选中区解析,见 startTableReferenceDrag)。 */
|
||||
let vgroupDragTableNames: string[] = [];
|
||||
|
||||
function tableVGroupDropTargetFor(payload: QueryEditorTableReferencePayload, event: MouseEvent) {
|
||||
if (!vgroupDragTableNames.length) return null;
|
||||
return resolveTableVGroupDropTarget(event.clientX, event.clientY, connectionStore.treeNodes, payload);
|
||||
}
|
||||
|
||||
function onTableReferenceMouseMove(event: MouseEvent) {
|
||||
if (!pendingTableReferenceDrag && !draggingTableReferencePayload) return;
|
||||
if (pendingTableReferenceDrag && !draggingTableReferencePayload) {
|
||||
@@ -1374,6 +1417,7 @@ function onTableReferenceMouseMove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
document.getSelection()?.removeAllRanges();
|
||||
referenceDragFeedback?.update(event.clientX, event.clientY);
|
||||
setTableVGroupDropTargetNodeId(tableVGroupDropTargetFor(draggingTableReferencePayload, event)?.node.id ?? null);
|
||||
// 仅查询编辑器消费 hover 光标线事件;AI 面板不监听。命中判定含覆盖层拦截时的几何回退。
|
||||
if (isOverSqlEditorTarget(event.clientX, event.clientY)) {
|
||||
window.dispatchEvent(createTableReferenceHoverEvent({ clientX: event.clientX, clientY: event.clientY }));
|
||||
@@ -1385,15 +1429,20 @@ function onTableReferenceMouseUp(event: MouseEvent) {
|
||||
const payload = draggingTableReferencePayload;
|
||||
if (payload) {
|
||||
suppressNextTableReferenceClick = true;
|
||||
const target = document.elementFromPoint(event.clientX, event.clientY);
|
||||
if (target instanceof Element && target.closest(`[data-query-editor-root], ${AI_ASSISTANT_TABLE_DROP_ROOT_SELECTOR}`)) {
|
||||
window.dispatchEvent(
|
||||
createTableReferenceDropEvent({
|
||||
payload,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
}),
|
||||
);
|
||||
const dropTarget = tableVGroupDropTargetFor(payload, event);
|
||||
if (dropTarget) {
|
||||
for (const tableName of vgroupDragTableNames) connectionStore.moveTableToVGroup(dropTarget.node, tableName, dropTarget.groupId);
|
||||
} 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}`)) {
|
||||
window.dispatchEvent(
|
||||
createTableReferenceDropEvent({
|
||||
payload,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
finishTableReferenceDrag();
|
||||
@@ -1566,7 +1615,6 @@ function onKeydown(event: KeyboardEvent) {
|
||||
rowWidthClass,
|
||||
{
|
||||
'group/sidebar-row': true,
|
||||
'ring-1 ring-primary/50 bg-primary/5': dragVisual.showInside,
|
||||
'opacity-50': dragVisual.dragging,
|
||||
'tree-item-connection-tint': connectionColor,
|
||||
'hover:bg-accent': node.type !== 'connection',
|
||||
@@ -1574,6 +1622,7 @@ function onKeydown(event: KeyboardEvent) {
|
||||
'tree-item-active': selectionVisual.rowSelected,
|
||||
'tree-item-active--selection-set': selectionVisual.usesSelectionSetHighlight && selectionVisual.rowSelected,
|
||||
'tree-item-highlight': highlighted,
|
||||
'ring-1 ring-primary/50 bg-primary/5': dragVisual.showInside || tableVGroupDropTargetNodeId === node.id,
|
||||
},
|
||||
]"
|
||||
:tabindex="selectionVisual.selected || selectionVisual.multiSelected ? 0 : -1"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fallbackCreateDatabaseCharsetMetadata } from "@/lib/database/createData
|
||||
import type { DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin";
|
||||
import type { AuthorizationPlan, AuthorizationStepResult } from "@/lib/database/databaseAuthorizationPlan";
|
||||
import type { MongoCreateIndexForm, MongoIndexRow } from "@/lib/sidebar/mongoCollectionMutation";
|
||||
import type { TableVGroupScope } from "@/lib/table/tableVGroup";
|
||||
|
||||
export type DuplicateStructureSource = TreeNode & { connectionId: string; database: string };
|
||||
type ConnectionDeleteTarget = TreeNode & { connectionId: string };
|
||||
@@ -20,6 +21,15 @@ export const sidebarFormTarget = shallowRef<TreeNode | null>(null);
|
||||
export const connectionDeleteTargetSnapshot = ref<ConnectionDeleteTarget[]>([]);
|
||||
export const connectionGroupDeleteTargetSnapshot = ref<ConnectionGroupDeleteTarget[]>([]);
|
||||
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 tableVGroupDialogParentGroupId = ref<string | null>(null);
|
||||
export const tableVGroupDialogTableNames = ref<string[]>([]);
|
||||
export const showTableVGroupDeleteConfirm = ref(false);
|
||||
/** Scope + group snapshot for the table vgroup delete confirmation. */
|
||||
export const tableVGroupDeleteTarget = shallowRef<{ scope: TableVGroupScope; groupId: string; name: string } | null>(null);
|
||||
export const showDeleteConfirm = ref(false);
|
||||
export const showDropTableConfirm = ref(false);
|
||||
export const showDropTableChildObjectConfirm = ref(false);
|
||||
@@ -235,6 +245,8 @@ const openFlags = [
|
||||
showCompileErrorDialog,
|
||||
showDeleteGroupConfirm,
|
||||
showMoveToNewGroupDialog,
|
||||
showTableVGroupDialog,
|
||||
showTableVGroupDeleteConfirm,
|
||||
];
|
||||
|
||||
export function resetSidebarTreeDialogState() {
|
||||
@@ -266,4 +278,7 @@ export function resetSidebarTreeDialogState() {
|
||||
connectionDeleteTargetSnapshot.value = [];
|
||||
connectionGroupDeleteTargetSnapshot.value = [];
|
||||
deleteConnectionsWithGroup.value = false;
|
||||
tableVGroupDialogScope.value = null;
|
||||
tableVGroupDialogParentGroupId.value = null;
|
||||
tableVGroupDialogTableNames.value = [];
|
||||
}
|
||||
Reference in new issue
Block a user