From d681c44232cbe1298e36ae9ebb60fc8da1b0be48 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 29 May 2026 08:39:25 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"=F0=9F=90=9B=20fix(sidebar):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=B0=E7=89=88=E5=B7=A6=E4=BE=A7=E5=88=86?= =?UTF-8?q?=E7=BB=84=E4=B8=8E=20Host=20=E6=8B=96=E6=8B=BD=E6=8E=92?= =?UTF-8?q?=E5=BA=8F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e4438780fe9faaa1913559b7d106e32ba835df40. --- .../Sidebar.locate-toolbar.test.tsx | 316 +------ frontend/src/components/Sidebar.tsx | 770 +++--------------- frontend/src/store.test.ts | 141 ---- frontend/src/store.ts | 467 +---------- 4 files changed, 163 insertions(+), 1531 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index bae29f7..06b19dd 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -9,27 +9,16 @@ import Sidebar, { filterV2ExplorerTreeByKind, getV2RailConnectionGroupBadgeText, hasSidebarLazyChildren, - normalizeSidebarTreeRelativeDropPosition, parseV2CommandSearchQuery, - resolveSidebarDropNodeFromDomEvent, - resolveSidebarTagDropInsertBefore, - resolveSidebarDropTargetMetricsFromDomEvent, - resolveSidebarDropInsertBefore, resolveSidebarNodeConnectionId, resolveV2ActiveConnectionId, isSidebarTablePinned, resolveSidebarTableNameForCopy, shouldClearSidebarActiveContextOnEmptySelect, - shouldSkipSidebarLoadOnExpandWhileDragging, - shouldSkipSidebarSelectWhileDragging, shouldLoadSidebarNodeOnExpand, sortSidebarTableEntries, } from './Sidebar'; -import { - buildSidebarRootConnectionToken, - buildSidebarRootTagToken, - buildSidebarTablePinKey, -} from '../store'; +import { buildSidebarTablePinKey } from '../store'; import { DEFAULT_SHORTCUT_OPTIONS, cloneShortcutOptions, @@ -69,36 +58,6 @@ const mocks = vi.hoisted(() => ({ })); vi.mock('../store', () => ({ - buildSidebarRootConnectionToken: (connectionId: string) => `connection:${connectionId.trim()}`, - buildSidebarRootTagToken: (tagId: string) => `tag:${tagId.trim()}`, - resolveSidebarRootOrderTokens: ( - sidebarRootOrder: unknown, - connectionTags: Array<{ id: string; connectionIds: string[] }>, - connections: Array<{ id: string }>, - ) => { - const groupedConnectionIds = new Set(); - connectionTags.forEach((tag) => tag.connectionIds.forEach((id) => groupedConnectionIds.add(id))); - const fallback = [ - ...connectionTags.map((tag) => `tag:${tag.id}`), - ...connections - .filter((conn) => !groupedConnectionIds.has(conn.id)) - .map((conn) => `connection:${conn.id}`), - ]; - const valid = new Set(fallback); - const normalized = Array.isArray(sidebarRootOrder) - ? sidebarRootOrder - .map((item) => String(item ?? '').trim()) - .filter((item) => valid.has(item)) - : []; - const seen = new Set(); - const result: string[] = []; - [...normalized, ...fallback].forEach((token) => { - if (!token || seen.has(token)) return; - seen.add(token); - result.push(token); - }); - return result; - }, buildSidebarTablePinKey: ( connectionId: string, dbName: string, @@ -124,14 +83,11 @@ vi.mock('../store', () => ({ setActiveContext: mocks.noop, removeConnection: mocks.noop, connectionTags: mocks.state.connectionTags, - sidebarRootOrder: [], addConnectionTag: mocks.noop, updateConnectionTag: mocks.noop, removeConnectionTag: mocks.noop, moveConnectionToTag: mocks.noop, - reorderConnections: mocks.noop, reorderTags: mocks.noop, - reorderSidebarRoot: mocks.noop, closeTabsByConnection: mocks.noop, closeTabsByDatabase: mocks.noop, theme: 'light', @@ -224,7 +180,7 @@ describe('Sidebar locate toolbar', () => { const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); expect(source).toContain('if (hasSidebarLazyChildren(children)) return;'); - expect(source).toContain('if (!shouldSkipSidebarLoadOnExpandWhileDragging(isTreeDragging, info))'); + expect(source).toContain('if (info?.expanded && shouldLoadSidebarNodeOnExpand(info.node))'); expect(source).toContain('if (shouldLoadSidebarNodeOnExpand(node))'); }); @@ -281,15 +237,6 @@ describe('Sidebar locate toolbar', () => { })).toBe('dev240'); }); - it('keeps the v2 active host empty when nothing is selected', () => { - expect(resolveV2ActiveConnectionId({ - activeContextConnectionId: '', - activeTabConnectionId: '', - selectedKeys: [], - connectionIds: ['local', 'dev240', 'dev241'], - })).toBe(''); - }); - it('does not clear v2 active context when rc-tree emits an empty deselect', () => { expect(shouldClearSidebarActiveContextOnEmptySelect(true)).toBe(false); expect(shouldClearSidebarActiveContextOnEmptySelect(false)).toBe(true); @@ -302,40 +249,20 @@ describe('Sidebar locate toolbar', () => { { id: 'local', name: 'local', config: { type: 'mysql', host: 'localhost' } }, ] as any[]; - const groups = buildV2RailConnectionGroups( - connections, - [{ - id: 'prod', - name: '生产环境', - connectionIds: ['dev241', 'missing', 'dev240'], - }], - [ - buildSidebarRootConnectionToken('local'), - buildSidebarRootTagToken('prod'), - ], - ); + const groups = buildV2RailConnectionGroups(connections, [{ + id: 'prod', + name: '生产环境', + connectionIds: ['dev241', 'missing', 'dev240'], + }]); expect(groups.map((group) => ({ id: group.id, name: group.name, isUngrouped: group.isUngrouped, - rootToken: group.rootToken, connectionIds: group.connections.map((conn) => conn.id), }))).toEqual([ - { - id: 'local', - name: 'local', - isUngrouped: true, - rootToken: buildSidebarRootConnectionToken('local'), - connectionIds: ['local'], - }, - { - id: 'prod', - name: '生产环境', - isUngrouped: undefined, - rootToken: buildSidebarRootTagToken('prod'), - connectionIds: ['dev241', 'dev240'], - }, + { id: 'prod', name: '生产环境', isUngrouped: undefined, connectionIds: ['dev241', 'dev240'] }, + { id: '__gonavi-v2-ungrouped-connections__', name: '未分组', isUngrouped: true, connectionIds: ['local'] }, ]); expect(getV2RailConnectionGroupBadgeText('Production')).toBe('PR'); expect(getV2RailConnectionGroupBadgeText('生产环境')).toBe('生'); @@ -358,7 +285,7 @@ describe('Sidebar locate toolbar', () => { }); it('renders the v2 sidebar rail, command search hint, filter tabs and log footer', () => { - const markup = renderToStaticMarkup(); + const markup = renderToStaticMarkup(); const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); expect(markup).toContain('gn-v2-sidebar-redesign'); @@ -388,14 +315,16 @@ describe('Sidebar locate toolbar', () => { expect(markup).toContain('data-sidebar-batch-database-action="true"'); expect(markup).toContain('data-sidebar-open-external-sql-file-action="true"'); expect(markup).toContain('data-sidebar-locate-current-tab-action="true"'); - expect(markup).toContain('data-gonavi-create-connection-action="true"'); expect(markup).toContain('aria-label="AI 助手"'); expect(markup).toContain('data-gonavi-ai-entry-action="true"'); expect(markup).toContain('aria-label="工具"'); expect(markup).toContain('data-gonavi-open-tools-action="true"'); expect(markup).toContain('aria-label="设置"'); - expect(source).toContain('buildV2RailConnectionGroups(connections, connectionTags, sidebarRootOrder)'); + expect(source).toContain('buildV2RailConnectionGroups(connections, connectionTags)'); + expect(source).toContain('data-v2-rail-connection-group="true"'); + expect(source).toContain('data-v2-rail-connection-group-header="true"'); expect(source).toContain("kind: 'v2-connection-group'"); + expect(source).toContain('data-v2-rail-host-context-menu-trigger="true"'); expect(source).toContain('onContextMenu={(event) => openV2ConnectionContextMenu(event, conn)}'); expect(source).toContain("kind: 'v2-connection'"); expect(source).toContain("if (contextMenu.kind === 'v2-connection') return () => renderV2ConnectionContextMenu(contextMenu.node);"); @@ -436,7 +365,6 @@ describe('Sidebar locate toolbar', () => { it('scales the v2 rail and footer tools from global appearance tokens', () => { const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8'); - expect(css).toMatch(/\.gn-v2-rail-action-group,\s*body\[data-ui-version="v2"\] \.gn-v2-rail-system-actions \{[^}]*flex-direction: column;/s); expect(css).toMatch(/\.gn-v2-rail-action-group,\s*body\[data-ui-version="v2"\] \.gn-v2-rail-system-actions \{[^}]*flex-direction: column;/s); expect(css).toMatch(/\.gn-v2-rail-action-group \{[^}]*border-bottom: 0\.5px solid var\(--gn-br-1\);/s); expect(css).toMatch(/\.gn-v2-explorer-toolbar\s*\{[^}]*display:\s*none\s*!important/s); @@ -446,11 +374,12 @@ describe('Sidebar locate toolbar', () => { expect(css).toMatch(/\.gn-v2-tree-title\.is-mono \.gn-v2-tree-label \{[^}]*font-size: clamp\(9px, calc\(var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\) - 1px\), 17px\);/s); expect(css).toMatch(/\.gn-v2-tree-count \{[^}]*font-size: clamp\(9px, calc\(var\(--gn-sidebar-tree-font-size, var\(--gn-font-size-sm, 12px\)\) - 2px\), 16px\);/s); expect(css).toMatch(/\.gn-v2-connection-rail \{[^}]*width: calc\(54px \* var\(--gn-ui-scale, 1\)\);[^}]*flex: 0 0 calc\(54px \* var\(--gn-ui-scale, 1\)\);/s); - expect(css).toMatch(/\.gn-v2-rail-item,[^}]*\.gn-v2-rail-tool \{[^}]*width: calc\(64px \* var\(--gn-ui-scale, 1\)\);[^}]*height: calc\(38px \* var\(--gn-ui-scale, 1\)\);[^}]*font-size: var\(--gn-font-size-sm, 12px\);/s); + expect(css).toMatch(/\.gn-v2-rail-items \{[^}]*padding-top: calc\(4px \* var\(--gn-ui-scale, 1\)\);/s); + expect(css).toMatch(/\.gn-v2-rail-group-header \{[^}]*overflow: visible;/s); + expect(css).toMatch(/\.gn-v2-rail-group-chevron \{[^}]*font-size: 10px;/s); + expect(css).toMatch(/\.gn-v2-rail-group-count \{[^}]*top: -1px;[^}]*right: -1px;[^}]*min-width: 16px;[^}]*height: 16px;[^}]*font-size: 9px;/s); + expect(css).toMatch(/\.gn-v2-rail-item,[^}]*\.gn-v2-rail-tool \{[^}]*width: calc\(38px \* var\(--gn-ui-scale, 1\)\);[^}]*height: calc\(38px \* var\(--gn-ui-scale, 1\)\);[^}]*font-size: var\(--gn-font-size-sm, 12px\);/s); expect(css).toMatch(/\.gn-v2-rail-tool \{[^}]*height: calc\(32px \* var\(--gn-ui-scale, 1\)\);/s); - expect(css).toMatch(/\.gn-v2-rail-tool \{[^}]*width: calc\(34px \* var\(--gn-ui-scale, 1\)\);/s); - expect(css).toMatch(/\.gn-v2-active-connection-trigger \{[^}]*height: 34px;[^}]*border: 0;[^}]*background: transparent;/s); - expect(css).not.toContain('.gn-v2-active-connection-trigger:hover'); }); it('keeps v2 tree status dots circular while truncating only the label text', () => { @@ -458,16 +387,12 @@ describe('Sidebar locate toolbar', () => { const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); expect(source).toContain('gn-v2-tree-status is-${status}'); - expect(source).toContain('data-sidebar-tree-folder-icon="true"'); - expect(css).toMatch(/\.gn-v2-tree-title\.is-connection \{[^}]*align-items:\s*center;/s); expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-title \{[^}]*overflow: visible;/s); expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-title > \.gn-v2-tree-title \{[^}]*overflow: visible;/s); expect(css).toMatch(/\.gn-v2-tree-status \{[^}]*width: 14px;[^}]*height: 14px;[^}]*flex: 0 0 14px;[^}]*overflow: visible;/s); expect(css).toMatch(/\.gn-v2-tree-status::before \{[^}]*width: 7px;[^}]*height: 7px;[^}]*border-radius: 50%;/s); expect(css).toMatch(/\.gn-v2-tree-status\.is-success::before \{[^}]*background: #22c55e;[^}]*box-shadow: 0 0 0 4px rgba\(34, 197, 94, 0\.18\);/s); expect(css).toMatch(/\.gn-v2-tree-label \{[^}]*overflow: hidden;[^}]*text-overflow: ellipsis;/s); - expect(css).toMatch(/\.gn-v2-tree-folder-icon \{[^}]*width: 22px;[^}]*height: 22px;[^}]*flex: 0 0 22px;/s); - expect(css).not.toContain('.gn-v2-tree-connection-meta'); }); it('does not repeat the active connection as an object-tree root in v2', () => { @@ -480,7 +405,7 @@ describe('Sidebar locate toolbar', () => { port: 3306, }, }]; - mocks.state.activeContext = { connectionId: 'conn-local', dbName: 'app_db' }; + mocks.state.activeContext = { connectionId: 'conn-local', dbName: '' }; mocks.state.activeTabId = ''; mocks.state.tabs = []; mocks.state.appearance = { @@ -495,39 +420,11 @@ describe('Sidebar locate toolbar', () => { expect(markup).toContain('gn-v2-connection-rail'); expect(markup).toContain('gn-v2-active-connection-copy'); expect(markup).toContain('本地'); - expect(markup).toContain('app_db'); - expect(markup).not.toContain('localhost'); + expect(markup).toContain('localhost'); expect(markup).not.toContain('gn-v2-db-icon-label'); }); - it('shows an empty v2 active host header when no host is selected', () => { - mocks.state.connections = [{ - id: 'conn-local', - name: '本地', - config: { - type: 'mysql', - host: 'localhost', - port: 3306, - }, - }]; - mocks.state.activeContext = null; - mocks.state.activeTabId = ''; - mocks.state.tabs = []; - mocks.state.appearance = { - enabled: true, - opacity: 1, - blur: 0, - uiVersion: 'v2', - }; - - const markup = renderToStaticMarkup(); - - expect(markup).toContain('未选择 Host'); - expect(markup).toContain('未选择数据库'); - expect(markup).not.toContain('本地'); - }); - - it('keeps all filter backed by the full tree so hosts remain visible in v2', () => { + it('renders existing connection tags as collapsible groups in the v2 rail', () => { mocks.state.connections = [ { id: 'dev240', @@ -564,168 +461,15 @@ describe('Sidebar locate toolbar', () => { }; const markup = renderToStaticMarkup(); - const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); - expect(source).toContain("if (v2ExplorerFilter === 'all') {"); - expect(source).toContain('gn-v2-tree-connection-copy'); - expect(source).not.toContain('gn-v2-tree-connection-meta'); - }); - - it('reorders dragged connections instead of only moving them between groups', () => { - const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); - - expect(source).toContain('const reorderConnections = useStore(state => state.reorderConnections);'); - expect(source).toContain('reorderConnections('); - expect(source).toContain('const insertBefore = resolveSidebarDropInsertBefore('); - expect(source).toContain('const domDropNode = resolveSidebarDropNodeFromDomEvent(info?.event);'); - expect(source).toContain('const dropTargetMetrics = resolveSidebarDropTargetMetricsFromDomEvent(info?.event);'); - expect(source).toContain("findTreeNodeByKeyRef.current(treeDataRef.current, domDropNode.key)"); - expect(source).toContain("const treeNode = baseElement.closest('.ant-tree-treenode') as HTMLElement | null;"); - expect(source).toContain('insertBefore,'); - }); - - it('reorders dragged tags relative to grouped connections instead of always appending them', () => { - const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); - - expect(source).toContain("connectionTags.find(t => t.connectionIds.includes(String(dropNode.key)))?.id || ''"); - expect(source).toContain('const dropTagId = dropNode.type === \'tag\''); - expect(source).toContain('if (dropTagId) {'); - }); - - it('wires v2 rail root dragging through the shared sidebar root order action', () => { - const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); - - expect(source).toContain('const reorderSidebarRoot = useStore(state => state.reorderSidebarRoot);'); - expect(source).toContain('const [draggingV2RailRootToken, setDraggingV2RailRootToken] = useState(\'\');'); - expect(source).toContain('const treeDragSelectSuppressUntilRef = useRef(0);'); - expect(source).toContain('const treeDragSelectionSnapshotRef = useRef<'); - expect(source).toContain('snapshotTreeSelectionBeforeDrag();'); - expect(source).toContain('restoreTreeSelectionAfterDrag();'); - expect(source).toContain('if (Date.now() < treeDragSelectSuppressUntilRef.current) {'); - expect(source).toContain('handleV2RailRootDrop('); - expect(source).toContain('draggable'); - expect(source).toContain('setDraggingV2RailRootToken(rootToken);'); - expect(source).toContain('reorderSidebarRoot(sourceToken, targetToken, insertBefore);'); - }); - - it('normalizes rc-tree absolute drop positions back to relative positions', () => { - expect(normalizeSidebarTreeRelativeDropPosition(4, '0-0-4')).toBe(0); - expect(normalizeSidebarTreeRelativeDropPosition(3, '0-0-4')).toBe(-1); - expect(normalizeSidebarTreeRelativeDropPosition(5, '0-0-4')).toBe(1); - }); - - it('resolves insert-before from either relative drop position or pointer position', () => { - expect(resolveSidebarDropInsertBefore(-1, null)).toBe(true); - expect(resolveSidebarDropInsertBefore(1, null)).toBe(false); - expect(resolveSidebarDropInsertBefore(0, { - clientY: 102, - top: 100, - height: 20, - })).toBe(true); - expect(resolveSidebarDropInsertBefore(0, { - clientY: 118, - top: 100, - height: 20, - })).toBe(false); - }); - - it('resolves sidebar drop node metadata from DOM markers', () => { - vi.stubGlobal('document', { - elementFromPoint: () => null, - }); - const marker = { - getAttribute: (name: string) => { - if (name === 'data-sidebar-node-key') return 'conn-a'; - if (name === 'data-sidebar-node-type') return 'connection'; - return null; - }, - }; - const target = { - closest: (selector: string) => selector === '[data-sidebar-node-key]' ? marker : null, - }; - - expect(resolveSidebarDropNodeFromDomEvent({ - target: target as unknown as EventTarget, - })).toEqual({ - key: 'conn-a', - type: 'connection', - }); - vi.unstubAllGlobals(); - }); - - it('resolves sidebar drop target metrics from the full tree row instead of nested children', () => { - vi.stubGlobal('document', { - elementFromPoint: () => null, - }); - const treeNode = { - getBoundingClientRect: () => ({ - top: 128, - height: 26, - }), - }; - const target = { - closest: (selector: string) => { - if (selector === '.ant-tree-treenode') return treeNode; - return null; - }, - }; - - expect(resolveSidebarDropTargetMetricsFromDomEvent({ - target: target as unknown as EventTarget, - })).toEqual({ - top: 128, - height: 26, - }); - vi.unstubAllGlobals(); - }); - - it('treats centered tag drops as directional reordering instead of no-op', () => { - expect(resolveSidebarTagDropInsertBefore({ - currentTagOrder: ['tag-dev', 'tag-test', 'tag-prod'], - dragTagId: 'tag-prod', - dropTagId: 'tag-dev', - relativeDropPosition: 0, - fallbackInsertBefore: false, - metrics: { - clientY: 113, - top: 100, - height: 26, - }, - })).toBe(true); - - expect(resolveSidebarTagDropInsertBefore({ - currentTagOrder: ['tag-dev', 'tag-test', 'tag-prod'], - dragTagId: 'tag-dev', - dropTagId: 'tag-prod', - relativeDropPosition: 0, - fallbackInsertBefore: true, - metrics: { - clientY: 113, - top: 100, - height: 26, - }, - })).toBe(false); - }); - - it('skips sidebar select side effects while tree dragging is active', () => { - expect(shouldSkipSidebarSelectWhileDragging(true, { selected: true })).toBe(true); - expect(shouldSkipSidebarSelectWhileDragging(false, { selected: false })).toBe(true); - expect(shouldSkipSidebarSelectWhileDragging(false, { selected: true })).toBe(false); - }); - - it('skips sidebar lazy load on expand while tree dragging is active', () => { - expect(shouldSkipSidebarLoadOnExpandWhileDragging(true, { - expanded: true, - node: { type: 'connection', children: undefined, isLeaf: false } as any, - })).toBe(true); - expect(shouldSkipSidebarLoadOnExpandWhileDragging(false, { - expanded: false, - node: { type: 'connection', children: undefined, isLeaf: false } as any, - })).toBe(true); - expect(shouldSkipSidebarLoadOnExpandWhileDragging(false, { - expanded: true, - node: { type: 'connection', children: undefined, isLeaf: false } as any, - })).toBe(false); + expect(markup).toContain('data-v2-rail-connection-group="true"'); + expect(markup).toContain('data-v2-rail-connection-group-header="true"'); + expect(markup).toContain('title="生产环境 · 1 个连接"'); + expect(markup).toContain('title="未分组 · 1 个连接"'); + expect(markup).toContain('aria-label="折叠连接分组 生产环境"'); + expect(markup).toContain('aria-label="切换到连接 dev240"'); + expect(markup).toContain('aria-label="切换到连接 本地"'); + expect(markup).toContain('data-v2-rail-host-context-menu-trigger="true"'); }); it('renders the v2 connection group context menu for rail group management', () => { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index d321e70..61b351f 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -44,13 +44,7 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge, StarFilled, StarOutlined } from '@ant-design/icons'; -import { - buildSidebarRootConnectionToken, - buildSidebarRootTagToken, - buildSidebarTablePinKey, - resolveSidebarRootOrderTokens, - useStore, -} from '../store'; +import { buildSidebarTablePinKey, useStore } from '../store'; import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme'; import { SavedConnection, ConnectionTag, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../types'; import { getDbIcon } from './DatabaseIcons'; @@ -242,17 +236,15 @@ export interface V2RailConnectionGroup { name: string; connections: SavedConnection[]; isUngrouped?: boolean; - rootToken: string; } export const buildV2RailConnectionGroups = ( connections: SavedConnection[], connectionTags: ConnectionTag[], - sidebarRootOrder: string[] = [], ): V2RailConnectionGroup[] => { const connectionById = new Map(connections.map((conn) => [conn.id, conn])); const groupedConnectionIds = new Set(); - const tagGroups = new Map(); + const groups: V2RailConnectionGroup[] = []; connectionTags.forEach((tag) => { const tagConnections: SavedConnection[] = []; @@ -263,62 +255,22 @@ export const buildV2RailConnectionGroups = ( tagConnections.push(conn); }); if (tagConnections.length === 0) return; - tagGroups.set(tag.id, { + groups.push({ id: tag.id, name: tag.name || '未命名分组', connections: tagConnections, - rootToken: buildSidebarRootTagToken(tag.id), }); }); - const ungroupedConnectionMap = new Map( - connections - .filter((conn) => !groupedConnectionIds.has(conn.id)) - .map((conn) => [conn.id, conn]), - ); - const orderedRootTokens = resolveSidebarRootOrderTokens( - sidebarRootOrder, - connectionTags, - connections, - ); - const groups: V2RailConnectionGroup[] = []; - - orderedRootTokens.forEach((token) => { - if (token.startsWith('tag:')) { - const tagId = token.slice('tag:'.length); - const group = tagGroups.get(tagId); - if (!group) return; - groups.push(group); - tagGroups.delete(tagId); - return; - } - if (token.startsWith('connection:')) { - const connectionId = token.slice('connection:'.length); - const conn = ungroupedConnectionMap.get(connectionId); - if (!conn) return; - groups.push({ - id: connectionId, - name: conn.name, - connections: [conn], - isUngrouped: true, - rootToken: buildSidebarRootConnectionToken(connectionId), - }); - ungroupedConnectionMap.delete(connectionId); - } - }); - - tagGroups.forEach((group) => { - groups.push(group); - }); - ungroupedConnectionMap.forEach((conn) => { + const ungroupedConnections = connections.filter((conn) => !groupedConnectionIds.has(conn.id)); + if (ungroupedConnections.length > 0) { groups.push({ - id: conn.id, - name: conn.name, - connections: [conn], + id: V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID, + name: groups.length > 0 ? '未分组' : '', + connections: ungroupedConnections, isUngrouped: true, - rootToken: buildSidebarRootConnectionToken(conn.id), }); - }); + } return groups; }; @@ -326,29 +278,9 @@ export const buildV2RailConnectionGroups = ( export const getV2RailConnectionGroupBadgeText = (name: unknown, fallback = '组'): string => { const trimmed = String(name ?? '').trim(); if (!trimmed) return fallback; - const cjkParts = trimmed.match(/[\u4e00-\u9fa5]/g); - if (cjkParts && cjkParts.length > 0) { - return cjkParts.slice(0, 1).join(''); - } - const latinTokens = trimmed.match(/[a-z0-9]+/gi) || []; - if (latinTokens.length >= 2) { - const firstToken = latinTokens[0] || ''; - const secondToken = latinTokens[1] || ''; - return `${firstToken[0] || ''}${secondToken[0] || ''}`.toUpperCase(); - } - if (latinTokens.length === 1) { - const token = latinTokens[0] || ''; - const alphaPrefix = token.match(/^[a-z]+/i)?.[0] || ''; - if (alphaPrefix) { - return alphaPrefix.slice(0, 2).toUpperCase(); - } - const trailingDigits = token.match(/(\d{2,})$/)?.[1]; - if (trailingDigits) { - return trailingDigits.slice(-2).toUpperCase(); - } - return token.slice(0, 2).toUpperCase(); - } - return trimmed.slice(0, 2); + const ascii = trimmed.replace(/[^a-z0-9]/gi, ''); + if (ascii.length >= 2) return ascii.slice(0, 2).toUpperCase(); + return trimmed.slice(0, 1); }; const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; label: string }> = [ @@ -509,168 +441,6 @@ export const resolveSidebarNodeConnectionId = ( return resolveSidebarConnectionIdFromKey(node?.key, connectionIds); }; -export const normalizeSidebarTreeRelativeDropPosition = ( - absoluteDropPosition: number, - nodePos: unknown, -): number => { - const segments = String(nodePos || '').split('-'); - const tailIndex = Number(segments[segments.length - 1] || 0); - return absoluteDropPosition - tailIndex; -}; - -export const resolveSidebarDropInsertBefore = ( - relativeDropPosition: number, - metrics?: { - clientY?: number; - top?: number; - height?: number; - } | null, -): boolean => { - if (relativeDropPosition < 0) return true; - if (relativeDropPosition > 0) return false; - const clientY = metrics?.clientY; - const top = metrics?.top; - const height = metrics?.height; - if ( - typeof clientY !== 'number' - || typeof top !== 'number' - || typeof height !== 'number' - || !Number.isFinite(clientY) - || !Number.isFinite(top) - || !Number.isFinite(height) - || height <= 0 - ) { - return false; - } - return clientY < (top + height / 2); -}; - -const resolveSidebarDropBaseElementFromDomEvent = ( - event: { - clientX?: number; - clientY?: number; - target?: EventTarget | null; - } | null | undefined, -): Element | null => { - if (typeof document === 'undefined') return null; - const fallbackTarget = event?.target && typeof (event.target as any).closest === 'function' - ? (event.target as unknown as Element) - : null; - const pointTarget = ( - typeof event?.clientX === 'number' - && typeof event?.clientY === 'number' - ) - ? document.elementFromPoint(event.clientX, event.clientY) - : null; - const baseElement = pointTarget || fallbackTarget; - if (!baseElement || typeof baseElement.closest !== 'function') return null; - return baseElement; -}; - -export const resolveSidebarDropNodeFromDomEvent = ( - event: { - clientX?: number; - clientY?: number; - target?: EventTarget | null; - } | null | undefined, -): { key: string; type: string } | null => { - const baseElement = resolveSidebarDropBaseElementFromDomEvent(event); - if (!baseElement) return null; - const marker = baseElement.closest('[data-sidebar-node-key]') as HTMLElement | null; - if (!marker) return null; - const key = String(marker.getAttribute('data-sidebar-node-key') || '').trim(); - const type = String(marker.getAttribute('data-sidebar-node-type') || '').trim(); - if (!key || !type) return null; - return { key, type }; -}; - -export const resolveSidebarDropTargetMetricsFromDomEvent = ( - event: { - clientX?: number; - clientY?: number; - target?: EventTarget | null; - } | null | undefined, -): { top: number; height: number } | null => { - const baseElement = resolveSidebarDropBaseElementFromDomEvent(event); - if (!baseElement) return null; - const treeNode = baseElement.closest('.ant-tree-treenode') as HTMLElement | null; - if (!treeNode || typeof treeNode.getBoundingClientRect !== 'function') return null; - const rect = treeNode.getBoundingClientRect(); - if (!Number.isFinite(rect.top) || !Number.isFinite(rect.height) || rect.height <= 0) { - return null; - } - return { - top: rect.top, - height: rect.height, - }; -}; - -export const resolveSidebarTagDropInsertBefore = (options: { - currentTagOrder: string[]; - dragTagId: string; - dropTagId: string; - relativeDropPosition: number; - fallbackInsertBefore: boolean; - metrics?: { - clientY?: number; - top?: number; - height?: number; - } | null; -}): boolean => { - const { - currentTagOrder, - dragTagId, - dropTagId, - relativeDropPosition, - fallbackInsertBefore, - metrics, - } = options; - - if (relativeDropPosition !== 0) { - return fallbackInsertBefore; - } - - const clientY = metrics?.clientY; - const top = metrics?.top; - const height = metrics?.height; - if ( - typeof clientY !== 'number' - || typeof top !== 'number' - || typeof height !== 'number' - || !Number.isFinite(clientY) - || !Number.isFinite(top) - || !Number.isFinite(height) - || height <= 0 - ) { - return fallbackInsertBefore; - } - - const ratio = (clientY - top) / height; - if (ratio < 0.35) return true; - if (ratio > 0.65) return false; - - const dragIndex = currentTagOrder.indexOf(dragTagId); - const dropIndex = currentTagOrder.indexOf(dropTagId); - if (dragIndex === -1 || dropIndex === -1 || dragIndex === dropIndex) { - return fallbackInsertBefore; - } - return dragIndex > dropIndex; -}; - -export const shouldSkipSidebarSelectWhileDragging = ( - isTreeDragging: boolean, - info: { selected?: boolean } | null | undefined, -): boolean => isTreeDragging || !info?.selected; - -export const shouldSkipSidebarLoadOnExpandWhileDragging = ( - isTreeDragging: boolean, - info: { expanded?: boolean; node?: Pick | null } | null | undefined, -): boolean => { - if (isTreeDragging) return true; - if (!info?.expanded) return true; - return !shouldLoadSidebarNodeOnExpand(info.node); -}; - export const resolveV2ActiveConnectionId = ({ activeContextConnectionId, activeTabConnectionId, @@ -697,6 +467,7 @@ export const resolveV2ActiveConnectionId = ({ || selectedConnectionId || normalizeDirectId(fallbackConnectionId) || normalizeDirectId(activeTabConnectionId) + || connectionIds[0] || ''; }; @@ -831,14 +602,11 @@ const Sidebar: React.FC<{ const setActiveContext = useStore(state => state.setActiveContext); const removeConnection = useStore(state => state.removeConnection); const connectionTags = useStore(state => state.connectionTags); - const sidebarRootOrder = useStore(state => state.sidebarRootOrder); const addConnectionTag = useStore(state => state.addConnectionTag); const updateConnectionTag = useStore(state => state.updateConnectionTag); const removeConnectionTag = useStore(state => state.removeConnectionTag); const moveConnectionToTag = useStore(state => state.moveConnectionToTag); - const reorderConnections = useStore(state => state.reorderConnections); const reorderTags = useStore(state => state.reorderTags); - const reorderSidebarRoot = useStore(state => state.reorderSidebarRoot); const closeTabsByConnection = useStore(state => state.closeTabsByConnection); const closeTabsByDatabase = useStore(state => state.closeTabsByDatabase); const theme = useStore(state => state.theme); @@ -933,16 +701,6 @@ const Sidebar: React.FC<{ const selectedNodesRef = useRef([]); const loadingNodesRef = useRef>(new Set()); const clickTimerRef = useRef | null>(null); - const treeDragSelectSuppressUntilRef = useRef(0); - const treeDragSelectionSnapshotRef = useRef<{ - selectedKeys: React.Key[]; - selectedNodes: any[]; - activeContext: { connectionId: string; dbName: string } | null; - }>({ - selectedKeys: [], - selectedNodes: [], - activeContext: null, - }); const driverStatusCacheRef = useRef<{ fetchedAt: number; items: Record } | null>(null); const driverUpdateWarningKeysRef = useRef>(new Set()); const connectionReloadSignaturesRef = useRef>({}); @@ -950,8 +708,8 @@ const Sidebar: React.FC<{ const [v2TableContextMenuStats, setV2TableContextMenuStats] = useState>({}); const connectionIds = useMemo(() => connections.map((conn) => conn.id), [connections]); const v2RailConnectionGroups = useMemo( - () => buildV2RailConnectionGroups(connections, connectionTags, sidebarRootOrder), - [connections, connectionTags, sidebarRootOrder], + () => buildV2RailConnectionGroups(connections, connectionTags), + [connections, connectionTags], ); const [collapsedV2RailGroupIds, setCollapsedV2RailGroupIds] = useState([]); const collapsedV2RailGroupIdSet = useMemo( @@ -959,23 +717,6 @@ const Sidebar: React.FC<{ [collapsedV2RailGroupIds], ); const hasV2RailConnectionGroups = v2RailConnectionGroups.some((group) => !group.isUngrouped); - const [draggingV2RailRootToken, setDraggingV2RailRootToken] = useState(''); - - const snapshotTreeSelectionBeforeDrag = useCallback(() => { - treeDragSelectionSnapshotRef.current = { - selectedKeys: [...selectedKeys], - selectedNodes: [...selectedNodesRef.current], - activeContext: activeContext ? { ...activeContext } : null, - }; - }, [activeContext, selectedKeys]); - - const restoreTreeSelectionAfterDrag = useCallback(() => { - const snapshot = treeDragSelectionSnapshotRef.current; - treeDragSelectSuppressUntilRef.current = Date.now() + 1000; - setSelectedKeys(snapshot.selectedKeys); - selectedNodesRef.current = snapshot.selectedNodes; - setActiveContext(snapshot.activeContext); - }, [setActiveContext]); const openV2CommandSearch = useCallback(() => { setIsV2CommandSearchOpen(true); @@ -1041,7 +782,6 @@ const Sidebar: React.FC<{ // Connection Status State: key -> 'success' | 'error' const [connectionStates, setConnectionStates] = useState>({}); - const [isTreeDragging, setIsTreeDragging] = useState(false); // Create Database Modal const [isCreateDbModalOpen, setIsCreateDbModalOpen] = useState(false); @@ -1212,20 +952,12 @@ const Sidebar: React.FC<{ }; const taggedConnIds = new Set(); - const tagNodesById = new Map(); - connectionTags.forEach((tag) => { + const tagNodes: TreeNode[] = connectionTags.map((tag) => { tag.connectionIds.forEach(id => taggedConnIds.add(id)); - tagNodesById.set(tag.id, { + return { title: tag.name, key: `tag-${tag.id}`, - icon: ( - - - - ), + icon: , type: 'tag', dataRef: tag, isLeaf: false, @@ -1233,43 +965,16 @@ const Sidebar: React.FC<{ .map(cid => connections.find(c => c.id === cid)) .filter(Boolean) .map(conn => buildConnectionNode(conn!)), - } as TreeNode); + } as TreeNode; }); - const ungroupedNodesById = new Map(); - connections + const ungroupedNodes: TreeNode[] = connections .filter(c => !taggedConnIds.has(c.id)) - .forEach((conn) => { - ungroupedNodesById.set(conn.id, buildConnectionNode(conn)); - }); + .map(conn => buildConnectionNode(conn)); - const orderedRootTokens = resolveSidebarRootOrderTokens( - sidebarRootOrder, - connectionTags, - connections, - ); - const orderedNodes: TreeNode[] = []; - orderedRootTokens.forEach((token) => { - if (token.startsWith('tag:')) { - const tagNode = tagNodesById.get(token.slice('tag:'.length)); - if (!tagNode) return; - orderedNodes.push(tagNode); - tagNodesById.delete(token.slice('tag:'.length)); - return; - } - if (token.startsWith('connection:')) { - const connectionNode = ungroupedNodesById.get(token.slice('connection:'.length)); - if (!connectionNode) return; - orderedNodes.push(connectionNode); - ungroupedNodesById.delete(token.slice('connection:'.length)); - } - }); - - orderedNodes.push(...Array.from(tagNodesById.values())); - orderedNodes.push(...Array.from(ungroupedNodesById.values())); - return orderedNodes; + return [...tagNodes, ...ungroupedNodes]; }); - }, [connections, connectionTags, sidebarRootOrder]); + }, [connections, connectionTags]); const handleDuplicateConnection = async (conn: SavedConnection) => { if (!conn?.id) return; @@ -2854,12 +2559,6 @@ const Sidebar: React.FC<{ if (isV2Ui && info?.node?.type === 'v2-table-section') { return; } - if (Date.now() < treeDragSelectSuppressUntilRef.current) { - return; - } - if (isTreeDragging) { - return; - } setSelectedKeys(keys); selectedNodesRef.current = info.selectedNodes || []; @@ -2869,7 +2568,7 @@ const Sidebar: React.FC<{ } return; } - if (shouldSkipSidebarSelectWhileDragging(isTreeDragging, info)) return; + if (!info.selected) return; const { type, dataRef, key, title } = info.node; const nodeConnectionId = resolveSidebarNodeConnectionId(info.node, connectionIds); @@ -2918,7 +2617,7 @@ const Sidebar: React.FC<{ const onExpand = (newExpandedKeys: React.Key[], info?: any) => { setExpandedKeys(newExpandedKeys); setAutoExpandParent(false); - if (!shouldSkipSidebarLoadOnExpandWhileDragging(isTreeDragging, info)) { + if (info?.expanded && shouldLoadSidebarNodeOnExpand(info.node)) { void onLoadData(info.node); } }; @@ -5447,14 +5146,8 @@ const Sidebar: React.FC<{ .map((node) => resolveSidebarNodeConnectionId(node, connectionIds)) .find(Boolean), }); - const activeConnection = connections.find((conn) => conn.id === activeConnectionId) || null; - const activeConnectionDisplayName = String(activeConnection?.name || '').trim() || '未选择 Host'; - const activeDatabaseDisplayName = useMemo(() => { - if (activeContext && typeof activeContext === 'object' && 'dbName' in activeContext) { - return String(activeContext.dbName || '').trim(); - } - return String(activeTab?.dbName || '').trim(); - }, [activeContext, activeTab?.dbName]); + const activeConnection = connections.find((conn) => conn.id === activeConnectionId) || connections[0] || null; + const activeConnectionHostSummary = resolveConnectionHostSummary(activeConnection?.config) || '未配置地址'; const activeConnectionTreeData = useMemo(() => { if (!activeConnection) return displayTreeData; const activeConnectionNode = displayTreeData.find((node) => node.type === 'connection' && node.key === activeConnection.id); @@ -5477,12 +5170,10 @@ const Sidebar: React.FC<{ const filtered = filterTree(displayTreeData); return filtered; }, [activeConnection, displayTreeData]); - const v2VisibleTreeData = useMemo(() => { - if (v2ExplorerFilter === 'all') { - return displayTreeData; - } - return filterV2ExplorerTreeByKind(activeConnectionTreeData, v2ExplorerFilter); - }, [activeConnectionTreeData, displayTreeData, v2ExplorerFilter]); + const v2VisibleTreeData = useMemo( + () => filterV2ExplorerTreeByKind(activeConnectionTreeData, v2ExplorerFilter), + [activeConnectionTreeData, v2ExplorerFilter], + ); const v2TreeMetrics = useMemo(() => { const databaseObjectCounts = new Map(); const objectGroupCounts = new Map(); @@ -5544,18 +5235,7 @@ const Sidebar: React.FC<{ )); }, []); - const handleV2RailRootDrop = useCallback(( - sourceToken: string, - targetToken: string, - insertBefore: boolean, - ) => { - if (!sourceToken || !targetToken || sourceToken === targetToken) { - return; - } - reorderSidebarRoot(sourceToken, targetToken, insertBefore); - }, [reorderSidebarRoot]); - - const getRailConnectionTypeLabel = (conn: SavedConnection): string => { + const getRailConnectionLabel = (conn: SavedConnection): string => { const iconType = resolveConnectionIconType(conn); if (iconType === 'mysql' || iconType === 'mariadb' || iconType === 'oceanbase') return 'MY'; if (iconType === 'postgres') return 'PG'; @@ -5569,67 +5249,6 @@ const Sidebar: React.FC<{ return iconType.slice(0, 2).toUpperCase() || 'DB'; }; - const getRailConnectionHostLabel = (conn: SavedConnection): string => { - const hostTokens = resolveConnectionHostTokens(conn.config); - const primaryHost = String(hostTokens[0] || '').trim().replace(/^\[|\]$/g, ''); - if (primaryHost) { - if (/^(localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0)$/i.test(primaryHost)) { - return 'LO'; - } - if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(primaryHost)) { - const lastSegment = primaryHost.split('.').pop() || ''; - return lastSegment.slice(-3).toUpperCase() || 'IP'; - } - if (primaryHost.includes(':') && /^[a-f0-9:]+$/i.test(primaryHost)) { - const lastSegment = primaryHost.split(':').filter(Boolean).pop() || ''; - return lastSegment.slice(-3).toUpperCase() || 'IP'; - } - - const hostFragments = primaryHost - .split(/[^a-z0-9\u4e00-\u9fa5]+/i) - .map((entry) => entry.trim()) - .filter(Boolean); - if (hostFragments.length >= 2) { - return `${hostFragments[0][0] || ''}${hostFragments[1][0] || ''}`.toUpperCase(); - } - const hostToken = hostFragments[0] || primaryHost.split('.')[0] || ''; - if (hostToken) { - return hostToken.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '').slice(0, 3).toUpperCase() || 'DB'; - } - } - - return getRailConnectionTypeLabel(conn); - }; - - const getRailConnectionBadgeLabel = (conn: SavedConnection): string => { - const connectionName = String(conn.name || '').trim(); - const cjkParts = connectionName.match(/[\u4e00-\u9fa5]/g); - if (cjkParts && cjkParts.length > 0) { - return cjkParts.slice(0, 2).join(''); - } - - const latinTokens = connectionName.match(/[a-z0-9]+/gi) || []; - if (latinTokens.length >= 2) { - const firstToken = latinTokens[0] || ''; - const secondToken = latinTokens[1] || ''; - return `${firstToken[0] || ''}${secondToken[0] || ''}`.toUpperCase(); - } - if (latinTokens.length === 1) { - const token = latinTokens[0]; - const alphaPrefix = token.match(/^[a-z]+/i)?.[0] || ''; - if (alphaPrefix) { - return alphaPrefix.slice(0, 3).toUpperCase(); - } - const trailingDigits = token.match(/(\d{2,})$/)?.[1]; - if (trailingDigits) { - return trailingDigits.slice(-3).toUpperCase(); - } - return token.slice(0, 3).toUpperCase(); - } - - return getRailConnectionTypeLabel(conn); - }; - const openV2ConnectionContextMenu = ( event: React.MouseEvent, connOrNode: SavedConnection | TreeNode, @@ -5652,10 +5271,6 @@ const Sidebar: React.FC<{ }; const getV2TreeMetaText = (node: any): string => { - if (node.type === 'tag') { - const count = flattenConnectionNodes(node.children || []).length; - return count > 0 ? count.toLocaleString() : ''; - } if (node.type === 'database') { const count = v2TreeMetrics.databaseObjectCounts.get(node.key) || 0; return count > 0 ? count.toLocaleString() : ''; @@ -5986,22 +5601,6 @@ const Sidebar: React.FC<{ {node?.dataRef?.pinnedSidebarTable ? : } ) : null; - if (node.type === 'connection') { - return ( - - {statusBadge} - - {displayTitle} - - - ); - } return ( <> {statusBadge} {displayTitle} @@ -7170,98 +6767,37 @@ const Sidebar: React.FC<{ }; const handleDrop = (info: any) => { - setIsTreeDragging(false); - const dropPosition = normalizeSidebarTreeRelativeDropPosition( - Number(info.dropPosition || 0), - info?.node?.pos, - ); - const domDropNode = resolveSidebarDropNodeFromDomEvent(info?.event); - const dropTargetMetrics = resolveSidebarDropTargetMetricsFromDomEvent(info?.event); - const insertBefore = resolveSidebarDropInsertBefore(dropPosition, dropTargetMetrics ? { - clientY: info?.event?.clientY, - top: dropTargetMetrics.top, - height: dropTargetMetrics.height, - } : null); + const dropKey = info.node.key; + const dragKey = info.dragNode.key; + const dropPos = info.node.pos.split('-'); + const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); const dragNode = info.dragNode; - const dropNode = domDropNode && domDropNode.key === String(info?.node?.key || '') - ? info.node - : (domDropNode - ? findTreeNodeByKeyRef.current(treeDataRef.current, domDropNode.key) || info.node - : info.node); + const dropNode = info.node; - const getDropRootToken = (node: any): string => { - if (!node) return ''; - if (node.type === 'tag') { - return buildSidebarRootTagToken(String(node?.dataRef?.id || '')); - } - if (node.type === 'connection') { - const groupedTagId = connectionTags.find((tag) => - tag.connectionIds.includes(String(node.key)), - )?.id || ''; - return groupedTagId - ? buildSidebarRootTagToken(groupedTagId) - : buildSidebarRootConnectionToken(String(node.key)); - } - return ''; - }; - - // Root tag or ungrouped connection reordering + // Tag to Tag reordering if (dragNode.type === 'tag') { + // You can only drop tags onto the root level (before/after other tags or connections at root) if (dropNode.type === 'tag' || dropNode.type === 'connection') { + // Get current order const currentTagOrder = connectionTags.map(t => t.id); const dragTagId = dragNode.dataRef.id; - const dropTagId = dropNode.type === 'tag' - ? dropNode.dataRef.id - : (connectionTags.find(t => t.connectionIds.includes(String(dropNode.key)))?.id || ''); - const dragRootToken = buildSidebarRootTagToken(String(dragTagId)); - const dropRootToken = getDropRootToken(dropNode); - - if (dropRootToken && dropRootToken !== dragRootToken) { - if (dropTagId) { - const resolvedInsertBefore = resolveSidebarTagDropInsertBefore({ - currentTagOrder, - dragTagId, - dropTagId, - relativeDropPosition: dropPosition, - fallbackInsertBefore: insertBefore, - metrics: dropTargetMetrics ? { - clientY: info?.event?.clientY, - top: dropTargetMetrics.top, - height: dropTargetMetrics.height, - } : null, - }); - reorderSidebarRoot(dragRootToken, dropRootToken, resolvedInsertBefore); - } else { - reorderSidebarRoot(dragRootToken, dropRootToken, insertBefore); - } - return; - } + // Filter out the dragging tag const newOrder = currentTagOrder.filter(id => id !== dragTagId); - let insertIndex = newOrder.length; - if (dropTagId) { - const dropIndex = newOrder.indexOf(dropTagId); - const resolvedInsertBefore = resolveSidebarTagDropInsertBefore({ - currentTagOrder, - dragTagId, - dropTagId, - relativeDropPosition: dropPosition, - fallbackInsertBefore: insertBefore, - metrics: dropTargetMetrics ? { - clientY: info?.event?.clientY, - top: dropTargetMetrics.top, - height: dropTargetMetrics.height, - } : null, - }); - if (resolvedInsertBefore) { + let insertIndex = newOrder.length; + if (dropNode.type === 'tag') { + const dropTagId = dropNode.dataRef.id; + const dropIndex = newOrder.indexOf(dropTagId); + + if (dropPosition === -1) { insertIndex = dropIndex; } else { insertIndex = dropIndex + 1; } } else { - // Dropped onto an ungrouped root connection, usually meaning moving to the end of tags + // Dropped onto a root connection, usually meaning moving to the end of tags // Since tags are always displayed before ungrouped connections, just put it at the end insertIndex = newOrder.length; } @@ -7272,38 +6808,27 @@ const Sidebar: React.FC<{ return; } - if (dragNode.type === 'connection') { - const dragTagId = connectionTags.find((tag) => - tag.connectionIds.includes(String(dragNode.key)), - )?.id || ''; - const dragIsUngroupedRoot = !dragTagId; - const dropRootToken = getDropRootToken(dropNode); - if (dragIsUngroupedRoot && dropNode.type === 'connection' && dropRootToken) { - reorderSidebarRoot( - buildSidebarRootConnectionToken(String(dragNode.key)), - dropRootToken, - insertBefore, - ); - return; - } - } - // Connection moving to tag (any drop position on a tag node counts as "into") if (dragNode.type === 'connection' && dropNode.type === 'tag') { moveConnectionToTag(dragNode.key, dropNode.dataRef.id); return; } - // Connection reordering against another connection + // Connection moving to another connection inside a tag if (dragNode.type === 'connection' && dropNode.type === 'connection') { + // Find if drop target is under a tag const targetTag = connectionTags.find(t => t.connectionIds.includes(dropNode.key)); - reorderConnections( - String(dragNode.key), - String(dropNode.key), - targetTag?.id || null, - insertBefore, - ); - return; + if (targetTag) { + moveConnectionToTag(dragNode.key, targetTag.id); + return; + } + + // Drop target is NOT under a tag (ungrouped) -> move OUT of tag + const sourceTag = connectionTags.find(t => t.connectionIds.includes(dragNode.key)); + if (sourceTag) { + moveConnectionToTag(dragNode.key, null); + return; + } } }; @@ -7366,51 +6891,14 @@ const Sidebar: React.FC<{ const renderV2RailConnectionButton = (conn: SavedConnection) => { const accent = resolveConnectionAccentColor(conn); const status = buildRailConnectionStatus(conn.id); - const badgeLabel = getRailConnectionBadgeLabel(conn); - const hostLabel = getRailConnectionHostLabel(conn); + const label = getRailConnectionLabel(conn); const title = `${conn.name} · ${resolveConnectionHostSummary(conn.config) || conn.config.type}`; - const rootToken = buildSidebarRootConnectionToken(conn.id); return ( ); @@ -7433,54 +6919,19 @@ const Sidebar: React.FC<{ const renderV2RailConnectionGroup = (group: V2RailConnectionGroup) => { const collapsed = collapsedV2RailGroupIdSet.has(group.id); const groupTitle = group.name || '连接'; - const rootToken = group.rootToken; + const groupLabel = getV2RailConnectionGroupBadgeText(group.name, group.isUngrouped ? '未' : '组'); return (
{ - snapshotTreeSelectionBeforeDrag(); - treeDragSelectSuppressUntilRef.current = Date.now() + 600; - setDraggingV2RailRootToken(rootToken); - setIsTreeDragging(true); - event.dataTransfer.effectAllowed = 'move'; - event.dataTransfer.setData('text/plain', rootToken); - }} - onDragEnd={() => { - restoreTreeSelectionAfterDrag(); - setDraggingV2RailRootToken(''); - setIsTreeDragging(false); - }} - onDragOver={(event) => { - if (!draggingV2RailRootToken || draggingV2RailRootToken === rootToken) { - return; - } - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'move'; - }} - onDrop={(event) => { - if (!draggingV2RailRootToken || draggingV2RailRootToken === rootToken) { - return; - } - event.preventDefault(); - event.stopPropagation(); - const rect = event.currentTarget.getBoundingClientRect(); - const insertBefore = event.clientY < rect.top + rect.height / 2; - handleV2RailRootDrop(draggingV2RailRootToken, rootToken, insertBefore); - restoreTreeSelectionAfterDrag(); - setDraggingV2RailRootToken(''); - setIsTreeDragging(false); - }} > {hasV2RailConnectionGroups && ( @@ -7519,9 +6970,23 @@ const Sidebar: React.FC<{ }; const renderV2ConnectionRail = () => ( -
+
+
+ {v2RailConnectionGroups.map(renderV2RailConnectionGroup)} + + + +
-
+
)}
@@ -7833,19 +7282,6 @@ const Sidebar: React.FC<{ icon: false, nodeDraggable: (node: any) => node.type === 'connection' || node.type === 'tag' }} - onDragStart={() => { - snapshotTreeSelectionBeforeDrag(); - treeDragSelectSuppressUntilRef.current = Date.now() + 600; - setIsTreeDragging(true); - }} - onDragEnter={() => { - treeDragSelectSuppressUntilRef.current = Date.now() + 600; - setIsTreeDragging(true); - }} - onDragEnd={() => { - restoreTreeSelectionAfterDrag(); - setIsTreeDragging(false); - }} onDrop={handleDrop} loadData={onLoadData} treeData={isV2Ui ? v2VisibleTreeData : displayTreeData} diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 9dae189..2c015b1 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -497,147 +497,6 @@ describe('store appearance persistence', () => { ); }); - it('reorders connections inside tags and ungrouped roots independently', async () => { - const { useStore } = await importStore(); - - useStore.getState().replaceConnections([ - { - id: 'conn-a', - name: 'A', - config: { id: 'conn-a', type: 'mysql', host: 'a.local', port: 3306, user: 'root' }, - }, - { - id: 'conn-b', - name: 'B', - config: { id: 'conn-b', type: 'mysql', host: 'b.local', port: 3306, user: 'root' }, - }, - { - id: 'conn-c', - name: 'C', - config: { id: 'conn-c', type: 'mysql', host: 'c.local', port: 3306, user: 'root' }, - }, - { - id: 'conn-d', - name: 'D', - config: { id: 'conn-d', type: 'mysql', host: 'd.local', port: 3306, user: 'root' }, - }, - ]); - useStore.getState().addConnectionTag({ - id: 'tag-dev', - name: '开发', - connectionIds: ['conn-b', 'conn-d'], - }); - - useStore.getState().reorderConnections('conn-d', 'conn-b', 'tag-dev', true); - expect(useStore.getState().connectionTags[0]?.connectionIds).toEqual(['conn-d', 'conn-b']); - - useStore.getState().reorderConnections('conn-c', 'conn-a', null, true); - expect(useStore.getState().connections.map((conn) => conn.id)).toEqual([ - 'conn-c', - 'conn-a', - 'conn-b', - 'conn-d', - ]); - }); - - it('reorders sidebar root items across tags and ungrouped hosts', async () => { - const { - buildSidebarRootConnectionToken, - buildSidebarRootTagToken, - resolveSidebarRootOrderTokens, - useStore, - } = await importStore(); - - useStore.getState().replaceConnections([ - { - id: 'conn-a', - name: 'A', - config: { id: 'conn-a', type: 'mysql', host: 'a.local', port: 3306, user: 'root' }, - }, - { - id: 'conn-b', - name: 'B', - config: { id: 'conn-b', type: 'mysql', host: 'b.local', port: 3306, user: 'root' }, - }, - { - id: 'conn-c', - name: 'C', - config: { id: 'conn-c', type: 'mysql', host: 'c.local', port: 3306, user: 'root' }, - }, - ]); - useStore.getState().addConnectionTag({ - id: 'tag-dev', - name: '开发', - connectionIds: ['conn-b'], - }); - - const initialOrder = resolveSidebarRootOrderTokens( - useStore.getState().sidebarRootOrder, - useStore.getState().connectionTags, - useStore.getState().connections, - ); - expect(initialOrder).toEqual([ - buildSidebarRootTagToken('tag-dev'), - buildSidebarRootConnectionToken('conn-a'), - buildSidebarRootConnectionToken('conn-c'), - ]); - - useStore.getState().reorderSidebarRoot( - buildSidebarRootTagToken('tag-dev'), - buildSidebarRootConnectionToken('conn-c'), - false, - ); - - expect(resolveSidebarRootOrderTokens( - useStore.getState().sidebarRootOrder, - useStore.getState().connectionTags, - useStore.getState().connections, - )).toEqual([ - buildSidebarRootConnectionToken('conn-a'), - buildSidebarRootConnectionToken('conn-c'), - buildSidebarRootTagToken('tag-dev'), - ]); - }); - - it('restores ungrouped host root order after moving a host out of a tag', async () => { - const { - buildSidebarRootConnectionToken, - buildSidebarRootTagToken, - resolveSidebarRootOrderTokens, - useStore, - } = await importStore(); - - useStore.getState().replaceConnections([ - { - id: 'conn-a', - name: 'A', - config: { id: 'conn-a', type: 'mysql', host: 'a.local', port: 3306, user: 'root' }, - }, - { - id: 'conn-b', - name: 'B', - config: { id: 'conn-b', type: 'mysql', host: 'b.local', port: 3306, user: 'root' }, - }, - ]); - useStore.getState().addConnectionTag({ - id: 'tag-dev', - name: '开发', - connectionIds: ['conn-b'], - }); - - useStore.getState().moveConnectionToTag('conn-a', 'tag-dev'); - useStore.getState().moveConnectionToTag('conn-a', null); - - expect(resolveSidebarRootOrderTokens( - useStore.getState().sidebarRootOrder, - useStore.getState().connectionTags, - useStore.getState().connections, - )).toEqual([ - buildSidebarRootTagToken('tag-dev'), - buildSidebarRootConnectionToken('conn-a'), - ]); - }); - it('keeps legacy global proxy password during hydration until explicit cleanup', async () => { storage.setItem('lite-db-storage', JSON.stringify({ state: { diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 7cbd9c5..6e550ee 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -851,169 +851,6 @@ const sanitizeConnectionTags = (value: unknown): ConnectionTag[] => { return result; }; -const SIDEBAR_ROOT_TAG_TOKEN_PREFIX = "tag:"; -const SIDEBAR_ROOT_CONNECTION_TOKEN_PREFIX = "connection:"; - -export const buildSidebarRootTagToken = (tagId: string): string => - `${SIDEBAR_ROOT_TAG_TOKEN_PREFIX}${toTrimmedString(tagId)}`; - -export const buildSidebarRootConnectionToken = ( - connectionId: string, -): string => `${SIDEBAR_ROOT_CONNECTION_TOKEN_PREFIX}${toTrimmedString(connectionId)}`; - -const isSidebarRootTagToken = (token: string): boolean => - token.startsWith(SIDEBAR_ROOT_TAG_TOKEN_PREFIX) && - token.length > SIDEBAR_ROOT_TAG_TOKEN_PREFIX.length; - -const isSidebarRootConnectionToken = (token: string): boolean => - token.startsWith(SIDEBAR_ROOT_CONNECTION_TOKEN_PREFIX) && - token.length > SIDEBAR_ROOT_CONNECTION_TOKEN_PREFIX.length; - -const sanitizeSidebarRootOrder = (value: unknown): string[] => { - if (!Array.isArray(value)) return []; - const seen = new Set(); - const result: string[] = []; - value.forEach((entry) => { - const token = toTrimmedString(entry); - if (!token) return; - if (!isSidebarRootTagToken(token) && !isSidebarRootConnectionToken(token)) { - return; - } - if (seen.has(token)) return; - seen.add(token); - result.push(token); - }); - return result; -}; - -const buildDefaultSidebarRootOrderTokens = ( - connectionTags: ConnectionTag[], - connections: SavedConnection[], -): string[] => { - const groupedConnectionIds = new Set(); - connectionTags.forEach((tag) => { - tag.connectionIds.forEach((connectionId) => { - if (connectionId) groupedConnectionIds.add(connectionId); - }); - }); - - return [ - ...connectionTags.map((tag) => buildSidebarRootTagToken(tag.id)), - ...connections - .filter((connection) => !groupedConnectionIds.has(connection.id)) - .map((connection) => buildSidebarRootConnectionToken(connection.id)), - ]; -}; - -export const resolveSidebarRootOrderTokens = ( - sidebarRootOrder: unknown, - connectionTags: ConnectionTag[], - connections: SavedConnection[], -): string[] => { - const defaultOrder = buildDefaultSidebarRootOrderTokens( - connectionTags, - connections, - ); - if (defaultOrder.length === 0) { - return []; - } - - const validTokens = new Set(defaultOrder); - const seen = new Set(); - const result: string[] = []; - - sanitizeSidebarRootOrder(sidebarRootOrder).forEach((token) => { - if (!validTokens.has(token) || seen.has(token)) return; - seen.add(token); - result.push(token); - }); - - defaultOrder.forEach((token) => { - if (seen.has(token)) return; - seen.add(token); - result.push(token); - }); - - return result; -}; - -const insertSidebarRootTokenBeforeUngrouped = ( - sidebarRootOrder: string[], - token: string, -): string[] => { - if (!token || sidebarRootOrder.includes(token)) { - return [...sidebarRootOrder]; - } - const firstConnectionIndex = sidebarRootOrder.findIndex( - isSidebarRootConnectionToken, - ); - if (firstConnectionIndex === -1) { - return [...sidebarRootOrder, token]; - } - const nextOrder = [...sidebarRootOrder]; - nextOrder.splice(firstConnectionIndex, 0, token); - return nextOrder; -}; - -const insertSidebarRootTokenAfter = ( - sidebarRootOrder: string[], - token: string, - anchorToken: string, -): string[] => { - if (!token) return [...sidebarRootOrder]; - const nextOrder = sidebarRootOrder.filter((item) => item !== token); - const anchorIndex = nextOrder.indexOf(anchorToken); - if (anchorIndex === -1) { - nextOrder.push(token); - return nextOrder; - } - nextOrder.splice(anchorIndex + 1, 0, token); - return nextOrder; -}; - -const moveSidebarRootToken = ( - sidebarRootOrder: string[], - sourceToken: string, - targetToken: string, - insertBefore: boolean, -): string[] => { - if (!sourceToken || !targetToken || sourceToken === targetToken) { - return [...sidebarRootOrder]; - } - const filtered = sidebarRootOrder.filter((token) => token !== sourceToken); - const targetIndex = filtered.indexOf(targetToken); - const insertIndex = - targetIndex === -1 - ? filtered.length - : Math.max( - 0, - Math.min( - filtered.length, - insertBefore ? targetIndex : targetIndex + 1, - ), - ); - filtered.splice(insertIndex, 0, sourceToken); - return filtered; -}; - -const orderConnectionTagsBySidebarRootOrder = ( - connectionTags: ConnectionTag[], - sidebarRootOrder: string[], -): ConnectionTag[] => { - const tagMap = new Map(connectionTags.map((tag) => [tag.id, tag])); - const orderedTags: ConnectionTag[] = []; - sidebarRootOrder.forEach((token) => { - if (!isSidebarRootTagToken(token)) return; - const tagId = token.slice(SIDEBAR_ROOT_TAG_TOKEN_PREFIX.length); - const tag = tagMap.get(tagId); - if (!tag) return; - orderedTags.push(tag); - tagMap.delete(tagId); - }); - orderedTags.push(...Array.from(tagMap.values())); - return orderedTags; -}; - const isLegacyDefaultAppearance = ( appearance: Partial<{ opacity: number; blur: number }> | undefined, ): boolean => { @@ -1050,7 +887,6 @@ export interface QueryOptions { interface AppState { connections: SavedConnection[]; connectionTags: ConnectionTag[]; - sidebarRootOrder: string[]; tabs: TabData[]; activeTabId: string | null; activeContext: { connectionId: string; dbName: string } | null; @@ -1119,18 +955,7 @@ interface AppState { connectionId: string, targetTagId: string | null, ) => void; - reorderConnections: ( - connectionId: string, - targetConnectionId: string, - targetTagId: string | null, - insertBefore?: boolean, - ) => void; reorderTags: (tagIds: string[]) => void; - reorderSidebarRoot: ( - sourceToken: string, - targetToken: string, - insertBefore: boolean, - ) => void; addTab: (tab: TabData) => void; updateQueryTabDraft: ( @@ -1847,7 +1672,6 @@ export const useStore = create()( (set) => ({ connections: [], connectionTags: [], - sidebarRootOrder: [], tabs: [], activeTabId: null, activeContext: null, @@ -1915,87 +1739,31 @@ export const useStore = create()( }; }), removeConnection: (id) => - set((state) => { - const nextConnections = state.connections.filter((c) => c.id !== id); - const nextTags = state.connectionTags.map((tag) => ({ + set((state) => ({ + connections: state.connections.filter((c) => c.id !== id), + connectionTags: state.connectionTags.map((tag) => ({ ...tag, connectionIds: tag.connectionIds.filter((cid) => cid !== id), - })); - return { - connections: nextConnections, - connectionTags: nextTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - state.sidebarRootOrder.filter( - (token) => token !== buildSidebarRootConnectionToken(id), - ), - nextTags, - nextConnections, - ), - }; - }), + })), + })), replaceConnections: (connections) => - set((state) => { - const nextConnections = sanitizeConnections(connections); - return { - connections: nextConnections, - sidebarRootOrder: resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - state.connectionTags, - nextConnections, - ), - shortcutOptions: - readPersistedShortcutOptions() ?? state.shortcutOptions, - }; - }), + set((state) => ({ + connections: sanitizeConnections(connections), + shortcutOptions: readPersistedShortcutOptions() ?? state.shortcutOptions, + })), addConnectionTag: (tag) => - set((state) => { - const nextTags = [...state.connectionTags, tag]; - const nextRootOrder = insertSidebarRootTokenBeforeUngrouped( - resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - state.connectionTags, - state.connections, - ), - buildSidebarRootTagToken(tag.id), - ); - return { - connectionTags: nextTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - nextRootOrder, - nextTags, - state.connections, - ), - }; - }), + set((state) => ({ connectionTags: [...state.connectionTags, tag] })), updateConnectionTag: (tag) => - set((state) => { - const nextTags = state.connectionTags.map((t) => + set((state) => ({ + connectionTags: state.connectionTags.map((t) => t.id === tag.id ? tag : t, - ); - return { - connectionTags: nextTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - nextTags, - state.connections, - ), - }; - }), + ), + })), removeConnectionTag: (id) => - set((state) => { - const nextTags = state.connectionTags.filter((t) => t.id !== id); - return { - connectionTags: nextTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - state.sidebarRootOrder.filter( - (token) => token !== buildSidebarRootTagToken(id), - ), - nextTags, - state.connections, - ), - }; - }), + set((state) => ({ + connectionTags: state.connectionTags.filter((t) => t.id !== id), + })), moveConnectionToTag: (connectionId, targetTagId) => set((state) => { const newTags = state.connectionTags.map((tag) => { @@ -2008,186 +1776,22 @@ export const useStore = create()( } return { ...tag, connectionIds: filteredIds }; }); - const nextRootOrder = resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - newTags, - state.connections, - ); - const connectionToken = buildSidebarRootConnectionToken(connectionId); - if (targetTagId) { - return { - connectionTags: newTags, - sidebarRootOrder: nextRootOrder.filter( - (token) => token !== connectionToken, - ), - }; - } - - const sourceToken = buildSidebarRootTagToken( - state.connectionTags.find((tag) => - tag.connectionIds.includes(connectionId), - )?.id || "", - ); - const insertedRootOrder = sourceToken - ? insertSidebarRootTokenAfter(nextRootOrder, connectionToken, sourceToken) - : insertSidebarRootTokenBeforeUngrouped(nextRootOrder, connectionToken); - return { - connectionTags: newTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - insertedRootOrder, - newTags, - state.connections, - ), - }; - }), - reorderConnections: ( - connectionId, - targetConnectionId, - targetTagId, - insertBefore = false, - ) => - set((state) => { - if ( - !connectionId || - !targetConnectionId || - connectionId === targetConnectionId - ) { - return { - connections: state.connections, - connectionTags: state.connectionTags, - }; - } - - const normalizeInsertIndex = ( - length: number, - index: number, - ): number => Math.max(0, Math.min(length, index)); - - const nextTags = state.connectionTags.map((tag) => ({ - ...tag, - connectionIds: tag.connectionIds.filter((id) => id !== connectionId), - })); - - if (targetTagId) { - const updatedTags = nextTags.map((tag) => { - if (tag.id !== targetTagId) { - return tag; - } - const targetIndex = tag.connectionIds.indexOf(targetConnectionId); - if (targetIndex === -1) { - return { - ...tag, - connectionIds: [...tag.connectionIds, connectionId], - }; - } - const insertIndex = normalizeInsertIndex( - tag.connectionIds.length, - insertBefore ? targetIndex : targetIndex + 1, - ); - const nextIds = [...tag.connectionIds]; - nextIds.splice(insertIndex, 0, connectionId); - return { ...tag, connectionIds: nextIds }; - }); - return { - connections: state.connections, - connectionTags: updatedTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - updatedTags, - state.connections, - ), - }; - } - - const ungroupedIds = state.connections - .map((conn) => conn.id) - .filter((id) => id !== connectionId) - .filter((id) => !nextTags.some((tag) => tag.connectionIds.includes(id))); - const targetIndex = ungroupedIds.indexOf(targetConnectionId); - const insertIndex = - targetIndex === -1 - ? ungroupedIds.length - : normalizeInsertIndex( - ungroupedIds.length, - insertBefore ? targetIndex : targetIndex + 1, - ); - const nextUngroupedIds = [...ungroupedIds]; - nextUngroupedIds.splice(insertIndex, 0, connectionId); - const ungroupedOrderMap = new Map( - nextUngroupedIds.map((id, index) => [id, index]), - ); - const nextConnections = [...state.connections].sort((a, b) => { - const indexA = ungroupedOrderMap.get(a.id); - const indexB = ungroupedOrderMap.get(b.id); - if (typeof indexA === 'number' && typeof indexB === 'number') { - return indexA - indexB; - } - if (typeof indexA === 'number') { - return -1; - } - if (typeof indexB === 'number') { - return 1; - } - return 0; - }); - - return { - connections: nextConnections, - connectionTags: nextTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - nextTags, - nextConnections, - ), - }; + return { connectionTags: newTags }; }), reorderTags: (tagIds) => set((state) => { - const nextRootOrder = resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - state.connectionTags, - state.connections, - ); - const orderedRootOrder = [ - ...tagIds.map((id) => buildSidebarRootTagToken(id)), - ...nextRootOrder.filter((token) => !isSidebarRootTagToken(token)), - ]; - const newTags = orderConnectionTagsBySidebarRootOrder( - state.connectionTags, - orderedRootOrder, - ); - return { - connectionTags: newTags, - sidebarRootOrder: resolveSidebarRootOrderTokens( - orderedRootOrder, - newTags, - state.connections, - ), - }; - }), - reorderSidebarRoot: (sourceToken, targetToken, insertBefore) => - set((state) => { - const nextRootOrder = moveSidebarRootToken( - resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - state.connectionTags, - state.connections, - ), - sourceToken, - targetToken, - insertBefore, - ); - return { - sidebarRootOrder: resolveSidebarRootOrderTokens( - nextRootOrder, - state.connectionTags, - state.connections, - ), - connectionTags: orderConnectionTagsBySidebarRootOrder( - state.connectionTags, - nextRootOrder, - ), - }; + const tagMap = new Map(state.connectionTags.map((t) => [t.id, t])); + const newTags: ConnectionTag[] = []; + tagIds.forEach((id) => { + const tag = tagMap.get(id); + if (tag) { + newTags.push(tag); + tagMap.delete(id); + } + }); + // 追加未指定的tag(如果有的话) + newTags.push(...Array.from(tagMap.values())); + return { connectionTags: newTags }; }), addTab: (tab) => @@ -2900,11 +2504,6 @@ export const useStore = create()( state.connectionTags, ); } - nextState.sidebarRootOrder = resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - nextState.connectionTags, - nextState.connections, - ); nextState.savedQueries = sanitizeSavedQueries(state.savedQueries); nextState.externalSQLDirectories = sanitizeExternalSQLDirectories( state.externalSQLDirectories, @@ -2976,11 +2575,6 @@ export const useStore = create()( ...state, connections: sanitizeConnections(state.connections), connectionTags: sanitizeConnectionTags(state.connectionTags), - sidebarRootOrder: resolveSidebarRootOrderTokens( - state.sidebarRootOrder, - sanitizeConnectionTags(state.connectionTags), - sanitizeConnections(state.connections), - ), tabs: safeTabs, activeTabId: sanitizeActiveTabId(state.activeTabId, safeTabs), savedQueries: sanitizeSavedQueries(state.savedQueries), @@ -3027,7 +2621,6 @@ export const useStore = create()( tabs, activeTabId: sanitizeActiveTabId(state.activeTabId, tabs), connectionTags: state.connectionTags, - sidebarRootOrder: state.sidebarRootOrder, savedQueries: state.savedQueries, externalSQLDirectories: state.externalSQLDirectories, theme: state.theme,