From af16c825684734c8cccf4d361ff0414d34b0a90a Mon Sep 17 00:00:00 2001 From: Euan Date: Fri, 31 Jul 2026 16:22:41 +0800 Subject: [PATCH] fix(sidebar): preserve table reference drag while filtering --- .../src/components/sidebar/ConnectionTree.vue | 13 +- .../src/components/sidebar/TreeItem.vue | 13 +- .../TreeItem.tableReferenceDrag.spec.ts | 149 ++++++++++++++++++ .../sidebar/sidebarFilterGuards.spec.ts | 2 +- 4 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/src/components/sidebar/__tests__/TreeItem.tableReferenceDrag.spec.ts diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 714fd8900..a220a8ade 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -1788,7 +1788,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
- +
{ - if (props.dragDisabled) return false; +const canReorderTreeNode = computed(() => { + if (props.reorderDisabled) return false; return activeNode.value.type === "connection" || activeNode.value.type === "connection-group"; }); @@ -869,7 +870,7 @@ const TABLE_REFERENCE_DRAG_THRESHOLD = 5; const TABLE_REFERENCE_DRAGGING_CLASS = "dbx-table-reference-dragging"; const canDragTableReference = computed(() => { - if (props.dragDisabled || !activeNode.value.connectionId) return false; + if (props.referenceDragDisabled || !activeNode.value.connectionId) return false; if (activeNode.value.type === "database") return typeof activeNode.value.database === "string" && activeNode.value.database.trim().length > 0; if (activeNode.value.database == null) return false; if (activeNode.value.type === "table" || activeNode.value.type === "view" || activeNode.value.type === "materialized_view") return true; @@ -986,7 +987,7 @@ function startTableReferenceMouseDrag(event: MouseEvent) { } function onRowMouseDown(event: MouseEvent) { - if (isDraggable.value) { + if (canReorderTreeNode.value) { startDrag(event, activeNode.value.id, activeNode.value.type); } else if (canDragTableReference.value) { startTableReferenceMouseDrag(event); diff --git a/apps/desktop/src/components/sidebar/__tests__/TreeItem.tableReferenceDrag.spec.ts b/apps/desktop/src/components/sidebar/__tests__/TreeItem.tableReferenceDrag.spec.ts new file mode 100644 index 000000000..f18bd47be --- /dev/null +++ b/apps/desktop/src/components/sidebar/__tests__/TreeItem.tableReferenceDrag.spec.ts @@ -0,0 +1,149 @@ +// @vitest-environment happy-dom + +import { createApp, defineComponent, h, nextTick, type App } from "vue"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import i18n from "@/i18n"; +import TreeItem from "@/components/sidebar/TreeItem.vue"; +import { DBX_TABLE_REFERENCE_DROP_EVENT, type QueryEditorTableReferenceDropDetail } from "@/lib/editor/queryEditorTableDrop"; +import { createSidebarTreeRuntime, sidebarTreeRuntimeKey } from "@/lib/sidebar/sidebarTreeRuntime"; +import type { TreeNode } from "@/types/database"; + +const connectionStore = { + activeConnectionId: "connection-1", + connectedIds: new Set(["connection-1"]), + connectingIds: new Set(), + connectionMultiSelectActive: false, + connections: [], + getConfig: () => ({ id: "connection-1", db_type: "sqlite" }), + isDefaultDatabase: () => false, + isPinnedTreeNodeReorderTarget: () => false, + isTreeNodeChildrenLoaded: () => false, + isTreeNodePinned: () => false, + selectedTreeNodeId: null, + selectedTreeNodeIds: [], + selectedTreeNodeIdsSet: new Set(), + sidebarTableSearchQueries: {}, + tableNameFilterForScope: () => undefined, + treeNodes: [], + treeSelectionAnchorId: null, +}; + +vi.mock("@/stores/connectionStore", () => ({ + useConnectionStore: () => connectionStore, +})); + +vi.mock("@/stores/queryStore", () => ({ + useQueryStore: () => ({ openDatabaseKeys: new Set() }), +})); + +vi.mock("@/stores/settingsStore", () => ({ + useSettingsStore: () => ({ + editorSettings: { + shortcuts: { openDataInNewTab: "" }, + sidebarActivation: "single", + sidebarAllowHorizontalScroll: false, + sidebarHiddenTablePrefixes: [], + sidebarObjectInfoMode: "none", + }, + }), +})); + +vi.mock("@/composables/useToast", () => ({ + useToast: () => ({ toast: vi.fn() }), +})); + +const tableNode: TreeNode = { + id: "table-orders", + label: "orders", + type: "table", + connectionId: "connection-1", + database: "main", +}; + +const mountedApps: App[] = []; +const dropListeners: EventListener[] = []; + +async function mountTreeItem(props: { reorderDisabled?: boolean; referenceDragDisabled?: boolean }) { + const container = document.createElement("div"); + document.body.append(container); + const app = createApp( + defineComponent({ + setup: () => () => + h(TreeItem, { + node: tableNode, + depth: 2, + ...props, + }), + }), + ); + mountedApps.push(app); + app.use(i18n); + app.provide(sidebarTreeRuntimeKey, createSidebarTreeRuntime()); + app.mount(container); + await nextTick(); + + const row = container.querySelector("[tabindex]"); + if (!row) throw new Error(`Tree item row was not rendered: ${container.innerHTML}`); + return row; +} + +function listenForTableReferenceDrop() { + const listener = vi.fn<(event: Event) => void>(); + dropListeners.push(listener); + window.addEventListener(DBX_TABLE_REFERENCE_DROP_EVENT, listener); + return listener; +} + +function mockEditorDropTarget() { + const editor = document.createElement("div"); + editor.dataset.queryEditorRoot = ""; + document.body.append(editor); + vi.spyOn(document, "elementFromPoint").mockReturnValue(editor); +} + +function dragToEditor(row: HTMLElement) { + row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0, clientX: 10, clientY: 10 })); + document.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, buttons: 1, clientX: 20, clientY: 20 })); + document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, button: 0, clientX: 30, clientY: 30 })); +} + +afterEach(() => { + for (const listener of dropListeners.splice(0)) window.removeEventListener(DBX_TABLE_REFERENCE_DROP_EVENT, listener); + for (const app of mountedApps.splice(0)) app.unmount(); + document.body.innerHTML = ""; + vi.restoreAllMocks(); +}); + +describe("TreeItem table reference dragging", () => { + it("keeps table reference dragging enabled when tree reordering is disabled", async () => { + mockEditorDropTarget(); + const onDrop = listenForTableReferenceDrop(); + + const row = await mountTreeItem({ reorderDisabled: true }); + dragToEditor(row); + + expect(onDrop).toHaveBeenCalledOnce(); + const detail = (onDrop.mock.calls[0][0] as CustomEvent).detail; + expect(detail).toEqual({ + payload: { + kind: "dbx-table-reference", + connectionId: "connection-1", + database: "main", + tableName: "orders", + databaseType: "sqlite", + }, + clientX: 30, + clientY: 30, + }); + }); + + it("does not start table reference dragging when it is explicitly disabled", async () => { + mockEditorDropTarget(); + const onDrop = listenForTableReferenceDrop(); + + const row = await mountTreeItem({ referenceDragDisabled: true }); + dragToEditor(row); + + expect(onDrop).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/sidebar/sidebarFilterGuards.spec.ts b/apps/desktop/src/lib/__tests__/sidebar/sidebarFilterGuards.spec.ts index d9b7f4188..db67d10cb 100644 --- a/apps/desktop/src/lib/__tests__/sidebar/sidebarFilterGuards.spec.ts +++ b/apps/desktop/src/lib/__tests__/sidebar/sidebarFilterGuards.spec.ts @@ -25,7 +25,7 @@ describe("sidebar filter guards", () => { expect(source).toContain("sidebarTableSearchEnabled && !isTreeSearchFiltering.value"); expect(source).toContain("!useVirtualTree.value || isTreeSearchFiltering.value"); expect(source.match(/if \(isRootListPartial\.value\)/g)).toHaveLength(2); - expect(source.match(/:drag-disabled="isRootListPartial \|\| isConnectionListAlphabeticallySorted"/g)).toHaveLength(2); + expect(source.match(/:reorder-disabled="isRootListPartial \|\| isConnectionListAlphabeticallySorted"/g)).toHaveLength(2); expect(source).not.toContain("isFiltering"); }); });