mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-10 23:11:40 +08:00
✨ feat(sql-files): 支持外部 SQL 目录树与双击打开
- 新增 SQL 目录选择、枚举与按路径读取接口,复用大文件执行能力 - Sidebar 增加外部 SQL 文件目录树、目录管理入口与双击打开查询标签 - 补充 external SQL 持久化与前后端回归测试 Fixes #319
This commit is contained in:
67
frontend/src/utils/externalSqlTree.test.ts
Normal file
67
frontend/src/utils/externalSqlTree.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types';
|
||||
import { buildExternalSQLRootNode, buildExternalSQLTabId } from './externalSqlTree';
|
||||
|
||||
describe('externalSqlTree helpers', () => {
|
||||
it('builds external SQL root node with nested directory and file entries', () => {
|
||||
const directories: ExternalSQLDirectory[] = [
|
||||
{
|
||||
id: 'dir-1',
|
||||
name: 'scripts',
|
||||
path: 'D:/sql/scripts',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'demo',
|
||||
createdAt: 1,
|
||||
},
|
||||
];
|
||||
const trees: Record<string, ExternalSQLTreeEntry[]> = {
|
||||
'dir-1': [
|
||||
{
|
||||
name: 'ddl',
|
||||
path: 'D:/sql/scripts/ddl',
|
||||
isDir: true,
|
||||
children: [
|
||||
{
|
||||
name: 'init.sql',
|
||||
path: 'D:/sql/scripts/ddl/init.sql',
|
||||
isDir: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const node = buildExternalSQLRootNode({
|
||||
dbNodeKey: 'conn-1-demo',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'demo',
|
||||
directories,
|
||||
directoryTrees: trees,
|
||||
});
|
||||
|
||||
expect(node.type).toBe('external-sql-root');
|
||||
expect(node.children).toHaveLength(1);
|
||||
expect(node.children?.[0]).toMatchObject({
|
||||
title: 'scripts',
|
||||
type: 'external-sql-directory',
|
||||
});
|
||||
expect(node.children?.[0].children?.[0]).toMatchObject({
|
||||
title: 'ddl',
|
||||
type: 'external-sql-folder',
|
||||
});
|
||||
expect(node.children?.[0].children?.[0].children?.[0]).toMatchObject({
|
||||
title: 'init.sql',
|
||||
type: 'external-sql-file',
|
||||
});
|
||||
});
|
||||
|
||||
it('builds query tab ids with connection and database isolation', () => {
|
||||
const first = buildExternalSQLTabId('conn-1', 'demo', 'D:/sql/init.sql');
|
||||
const second = buildExternalSQLTabId('conn-1', 'demo2', 'D:/sql/init.sql');
|
||||
|
||||
expect(first).toContain('conn-1');
|
||||
expect(first).toContain('demo');
|
||||
expect(first).not.toBe(second);
|
||||
});
|
||||
});
|
||||
131
frontend/src/utils/externalSqlTree.ts
Normal file
131
frontend/src/utils/externalSqlTree.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types';
|
||||
|
||||
export type ExternalSQLNodeType =
|
||||
| 'external-sql-root'
|
||||
| 'external-sql-directory'
|
||||
| 'external-sql-folder'
|
||||
| 'external-sql-file';
|
||||
|
||||
export interface ExternalSQLTreeNode {
|
||||
title: string;
|
||||
key: string;
|
||||
isLeaf?: boolean;
|
||||
children?: ExternalSQLTreeNode[];
|
||||
type: ExternalSQLNodeType;
|
||||
dataRef: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type BuildExternalSQLRootNodeParams = {
|
||||
dbNodeKey: string;
|
||||
connectionId: string;
|
||||
dbName: string;
|
||||
directories: ExternalSQLDirectory[];
|
||||
directoryTrees: Record<string, ExternalSQLTreeEntry[]>;
|
||||
};
|
||||
|
||||
const normalizeExternalSQLPath = (value: string): string =>
|
||||
String(value || '').trim().replace(/\\/g, '/');
|
||||
|
||||
const resolveDirectoryDisplayName = (directory: ExternalSQLDirectory): string => {
|
||||
const explicitName = String(directory.name || '').trim();
|
||||
if (explicitName) return explicitName;
|
||||
const normalizedPath = normalizeExternalSQLPath(directory.path);
|
||||
const segments = normalizedPath.split('/').filter(Boolean);
|
||||
return segments[segments.length - 1] || 'SQL目录';
|
||||
};
|
||||
|
||||
export const buildExternalSQLDirectoryId = (connectionId: string, dbName: string, directoryPath: string): string =>
|
||||
`external-sql-dir:${String(connectionId || '').trim()}:${String(dbName || '').trim()}:${normalizeExternalSQLPath(directoryPath)}`;
|
||||
|
||||
export const buildExternalSQLTabId = (connectionId: string, dbName: string, filePath: string): string =>
|
||||
`external-sql-tab:${String(connectionId || '').trim()}:${String(dbName || '').trim()}:${normalizeExternalSQLPath(filePath)}`;
|
||||
|
||||
const buildExternalSQLNodeKey = (type: ExternalSQLNodeType, base: string): string =>
|
||||
`${type}:${normalizeExternalSQLPath(base)}`;
|
||||
|
||||
const mapExternalSQLTreeEntries = (
|
||||
entries: ExternalSQLTreeEntry[],
|
||||
context: { connectionId: string; dbName: string; dbNodeKey: string; directoryId: string },
|
||||
): ExternalSQLTreeNode[] => entries.map((entry) => {
|
||||
const entryPath = normalizeExternalSQLPath(entry.path);
|
||||
if (entry.isDir) {
|
||||
const children = mapExternalSQLTreeEntries(entry.children || [], context);
|
||||
return {
|
||||
title: entry.name,
|
||||
key: buildExternalSQLNodeKey('external-sql-folder', entryPath),
|
||||
type: 'external-sql-folder',
|
||||
isLeaf: children.length === 0,
|
||||
children: children.length > 0 ? children : undefined,
|
||||
dataRef: {
|
||||
connectionId: context.connectionId,
|
||||
dbName: context.dbName,
|
||||
dbNodeKey: context.dbNodeKey,
|
||||
directoryId: context.directoryId,
|
||||
path: entry.path,
|
||||
name: entry.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: entry.name,
|
||||
key: buildExternalSQLNodeKey('external-sql-file', entryPath),
|
||||
type: 'external-sql-file',
|
||||
isLeaf: true,
|
||||
dataRef: {
|
||||
connectionId: context.connectionId,
|
||||
dbName: context.dbName,
|
||||
dbNodeKey: context.dbNodeKey,
|
||||
directoryId: context.directoryId,
|
||||
path: entry.path,
|
||||
name: entry.name,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const buildExternalSQLRootNode = ({
|
||||
dbNodeKey,
|
||||
connectionId,
|
||||
dbName,
|
||||
directories,
|
||||
directoryTrees,
|
||||
}: BuildExternalSQLRootNodeParams): ExternalSQLTreeNode => {
|
||||
const sortedDirectories = [...directories].sort((left, right) =>
|
||||
resolveDirectoryDisplayName(left).toLowerCase().localeCompare(resolveDirectoryDisplayName(right).toLowerCase()),
|
||||
);
|
||||
|
||||
const children = sortedDirectories.map((directory) => {
|
||||
const directoryChildren = mapExternalSQLTreeEntries(directoryTrees[directory.id] || [], {
|
||||
connectionId,
|
||||
dbName,
|
||||
dbNodeKey,
|
||||
directoryId: directory.id,
|
||||
});
|
||||
return {
|
||||
title: resolveDirectoryDisplayName(directory),
|
||||
key: buildExternalSQLNodeKey('external-sql-directory', directory.id),
|
||||
type: 'external-sql-directory' as const,
|
||||
isLeaf: directoryChildren.length === 0,
|
||||
children: directoryChildren.length > 0 ? directoryChildren : undefined,
|
||||
dataRef: {
|
||||
...directory,
|
||||
connectionId,
|
||||
dbName,
|
||||
dbNodeKey,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: children.length > 0 ? `外部 SQL 文件 (${children.length})` : '外部 SQL 文件',
|
||||
key: `${dbNodeKey}-external-sql`,
|
||||
type: 'external-sql-root',
|
||||
isLeaf: children.length === 0,
|
||||
children: children.length > 0 ? children : undefined,
|
||||
dataRef: {
|
||||
connectionId,
|
||||
dbName,
|
||||
dbNodeKey,
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user