mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-12 01:24:12 +08:00
✨ feat(query): 新建查询继承当前表上下文 (#874)
## 变更说明 - 当前活动标签为表数据页或表设计页时,新建查询会自动继承连接、数据库和表名上下文 - 按数据库方言正确引用表名,并为 Elasticsearch 生成原生查询模板 - 只有活动表与用户最新选择的连接、数据库一致时,才继承表名,避免跨数据库误带旧表上下文 - 无有效表上下文时保持原有新建查询行为 - 自定义模板仅在以 FROM 结尾、存在明确表名插入点时自动补全,避免破坏用户模板 ## 验证 - `npm test -- src/components/QueryEditor.external-sql-save.test.tsx src/utils/newQueryContext.test.ts src/utils/objectQueryTemplates.test.ts`:344 项测试通过 - `npm run build`:TypeScript 与 Vite 生产构建通过 - 已基于最新 `dev`(包含 #872/#870/#875)解决 `App.tsx` 冲突 Closes #849
This commit is contained in:
@@ -102,6 +102,8 @@ import {
|
||||
import { downloadBrowserTextFile } from './utils/browserFileTransfer';
|
||||
import { buildDataSyncWorkbenchTab } from './utils/dataSyncTab';
|
||||
import { buildSqlAuditWorkbenchTab } from './utils/sqlAuditTab';
|
||||
import { resolveDataSourceType } from './utils/dataSourceCapabilities';
|
||||
import { buildContextualNewQueryTemplate } from './utils/objectQueryTemplates';
|
||||
import {
|
||||
extractCustomThemeAntTokens,
|
||||
} from './utils/customTheme';
|
||||
@@ -236,7 +238,7 @@ import {
|
||||
import { useAppUpdateManager } from './hooks/useAppUpdateManager';
|
||||
import { useAppLogPanelResize } from './hooks/useAppLogPanelResize';
|
||||
import { useAppSidebarResize } from './hooks/useAppSidebarResize';
|
||||
import { resolveNewQueryContext } from './utils/newQueryContext';
|
||||
import { canInheritNewQueryTableContext, resolveNewQueryContext } from './utils/newQueryContext';
|
||||
import { useAppUtilityStyles } from './hooks/useAppUtilityStyles';
|
||||
import { useWorkbenchTabs } from './hooks/useWorkbenchTabs';
|
||||
import {
|
||||
@@ -2728,6 +2730,19 @@ function App() {
|
||||
activeTab: currentTab,
|
||||
validConnectionIds: new Set(connections.map(connection => connection.id)),
|
||||
});
|
||||
const connection = connections.find(c => c.id === targetContext.connectionId);
|
||||
const inheritsTableContext = canInheritNewQueryTableContext({
|
||||
activeTab: currentTab,
|
||||
targetContext,
|
||||
});
|
||||
const tableName = inheritsTableContext ? String(currentTab?.tableName || '').trim() : '';
|
||||
const contextualQuery = tableName && connection
|
||||
? buildContextualNewQueryTemplate({
|
||||
dbType: resolveDataSourceType(connection.config),
|
||||
tableName,
|
||||
customTemplate: appearance.newQuerySqlTemplate,
|
||||
})
|
||||
: null;
|
||||
|
||||
addTab({
|
||||
id: `query-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
@@ -2735,9 +2750,9 @@ function App() {
|
||||
type: 'query',
|
||||
connectionId: targetContext.connectionId,
|
||||
dbName: targetContext.dbName,
|
||||
query: ''
|
||||
query: contextualQuery ?? '',
|
||||
});
|
||||
}, [activeTabId, tabs, connections, activeContext, addTab, t]);
|
||||
}, [activeTabId, tabs, connections, activeContext, addTab, appearance.newQuerySqlTemplate, t]);
|
||||
|
||||
const switchActiveTabByOffset = useCallback((offset: 1 | -1) => {
|
||||
if (tabs.length < 2) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveNewQueryContext } from './newQueryContext';
|
||||
import { canInheritNewQueryTableContext, resolveNewQueryContext } from './newQueryContext';
|
||||
|
||||
describe('resolveNewQueryContext', () => {
|
||||
const validConnectionIds = new Set(['conn-a', 'conn-b']);
|
||||
@@ -44,4 +44,20 @@ describe('resolveNewQueryContext', () => {
|
||||
validConnectionIds,
|
||||
})).toEqual({ connectionId: '', dbName: '' });
|
||||
});
|
||||
|
||||
it('only inherits a table tab when it belongs to the resolved target context', () => {
|
||||
const tableTab = { type: 'table', connectionId: 'conn-a', dbName: 'database_a', tableName: 'users' };
|
||||
expect(canInheritNewQueryTableContext({
|
||||
activeTab: tableTab,
|
||||
targetContext: { connectionId: 'conn-a', dbName: 'database_a' },
|
||||
})).toBe(true);
|
||||
expect(canInheritNewQueryTableContext({
|
||||
activeTab: tableTab,
|
||||
targetContext: { connectionId: 'conn-b', dbName: 'database_b' },
|
||||
})).toBe(false);
|
||||
expect(canInheritNewQueryTableContext({
|
||||
activeTab: { ...tableTab, type: 'query' },
|
||||
targetContext: { connectionId: 'conn-a', dbName: 'database_a' },
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,3 +35,20 @@ export const resolveNewQueryContext = ({
|
||||
|| normalizeValidContext(activeTab, validConnectionIds)
|
||||
|| { connectionId: '', dbName: '' }
|
||||
);
|
||||
export interface NewQueryTableContextLike extends NewQueryContextLike {
|
||||
type?: unknown;
|
||||
tableName?: unknown;
|
||||
}
|
||||
|
||||
export const canInheritNewQueryTableContext = ({
|
||||
activeTab,
|
||||
targetContext,
|
||||
}: {
|
||||
activeTab?: NewQueryTableContextLike | null;
|
||||
targetContext: NewQueryContext;
|
||||
}): boolean => {
|
||||
const tabType = String(activeTab?.type || '');
|
||||
return (tabType === 'table' || tabType === 'design')
|
||||
&& String(activeTab?.connectionId || '').trim() === targetContext.connectionId
|
||||
&& String(activeTab?.dbName || '').trim() === targetContext.dbName;
|
||||
};
|
||||
|
||||
@@ -9,12 +9,47 @@ vi.mock('../../wailsjs/go/app/App', () => ({
|
||||
}));
|
||||
|
||||
import {
|
||||
buildContextualNewQueryTemplate,
|
||||
buildTableSelectQuery,
|
||||
extractTableSelectColumnNames,
|
||||
isElasticsearchDbType,
|
||||
resolveTableSelectQuery,
|
||||
} from './objectQueryTemplates';
|
||||
|
||||
describe('buildContextualNewQueryTemplate', () => {
|
||||
it('builds a dialect-aware select for the active table with the default template', () => {
|
||||
expect(buildContextualNewQueryTemplate({
|
||||
dbType: 'mysql',
|
||||
tableName: 'Order Items',
|
||||
customTemplate: null,
|
||||
})).toBe('SELECT * FROM `Order Items`;');
|
||||
});
|
||||
|
||||
it('appends the active table when a custom template ends with FROM', () => {
|
||||
expect(buildContextualNewQueryTemplate({
|
||||
dbType: 'postgres',
|
||||
tableName: 'public.OrderItems',
|
||||
customTemplate: 'SELECT id, total FROM ',
|
||||
})).toBe('SELECT id, total FROM public."OrderItems"');
|
||||
});
|
||||
|
||||
it('preserves custom templates that do not expose a table insertion point', () => {
|
||||
expect(buildContextualNewQueryTemplate({
|
||||
dbType: 'mysql',
|
||||
tableName: 'users',
|
||||
customTemplate: 'SELECT CURRENT_TIMESTAMP;',
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
it('builds the native query format for an active Elasticsearch index', () => {
|
||||
expect(buildContextualNewQueryTemplate({
|
||||
dbType: 'elasticsearch',
|
||||
tableName: 'orders-v1',
|
||||
customTemplate: null,
|
||||
})).toBe('GET /orders-v1/_search\n{\n "query": {\n "match_all": {}\n }\n}\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTableSelectQuery', () => {
|
||||
it('quotes uppercase postgres table names in new query templates', () => {
|
||||
expect(buildTableSelectQuery('postgres', 'public.MyTable')).toBe('SELECT * FROM public."MyTable";');
|
||||
|
||||
@@ -59,6 +59,34 @@ export const buildTableSelectQuery = (
|
||||
return `SELECT\n ${selectList}\nFROM ${quotedTable}${limitSuffix};`;
|
||||
};
|
||||
|
||||
type BuildContextualNewQueryTemplateOptions = {
|
||||
dbType: string;
|
||||
tableName: string;
|
||||
customTemplate?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the initial query for a new tab opened while a table-like data tab is active.
|
||||
* A custom template is only augmented when it explicitly ends at a FROM insertion point.
|
||||
*/
|
||||
export const buildContextualNewQueryTemplate = ({
|
||||
dbType,
|
||||
tableName,
|
||||
customTemplate,
|
||||
}: BuildContextualNewQueryTemplateOptions): string | null => {
|
||||
const normalizedTableName = String(tableName || '').trim();
|
||||
if (!normalizedTableName) return null;
|
||||
if (isElasticsearchDbType(dbType) || customTemplate === null || customTemplate === undefined) {
|
||||
return buildTableSelectQuery(dbType, normalizedTableName);
|
||||
}
|
||||
|
||||
const template = String(customTemplate)
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n');
|
||||
if (!/\bfrom\s*$/i.test(template)) return null;
|
||||
return `${template}${quoteQualifiedIdent(dbType, normalizedTableName)}`;
|
||||
};
|
||||
|
||||
type ResolveTableSelectQueryOptions = {
|
||||
dbType: string;
|
||||
tableName: string;
|
||||
|
||||
Reference in New Issue
Block a user