From 0fe3060cf1291314c71c444dfe75b5ab47ba0ec0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 2 Jul 2026 17:50:56 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20style(query-editor):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=A1=A5=E5=85=A8=E5=80=99=E9=80=89=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 候选行未选中时也显示元数据,避免只有当前选中项可见 - 将字段名与元数据改为紧凑排列,减少候选列表空间浪费 - 增加表备注、字段类型备注、当前库隔离和候选样式回归断言 --- frontend/src/App.css | 21 +++ .../QueryEditor.external-sql-save.test.tsx | 149 ++++++++++++++++-- 2 files changed, 160 insertions(+), 10 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index a1773110..51cc7c71 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -247,6 +247,27 @@ body[data-theme='light'] ::-webkit-scrollbar-thumb:hover { min-height: 260px; } +.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main { + justify-content: flex-start; + gap: 6px; +} + +.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left { + flex: 0 1 auto; + min-width: 0; +} + +.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right { + flex: 1 1 auto; + max-width: none; + min-width: 0; +} + +.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .right > .details-label { + display: inline !important; + margin-left: 0; +} + /* Ensure body background matches theme to avoid white flashes, but kept transparent for window composition */ body { transition: color 0.3s; diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index d62f7e2e..2f9dbd35 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -1490,6 +1490,54 @@ describe('QueryEditor external SQL save', () => { }); }); + it('does not suggest tables from other databases for unqualified FROM completion', async () => { + let renderer!: ReactTestRenderer; + autoFetchState.visible = true; + storeState.connections[0].config.database = 'mkefu_ai_dev'; + backendApp.DBGetDatabases.mockResolvedValueOnce({ + success: true, + data: [{ Database: 'mkefu_ai_dev' }, { Database: 'mkefu_dev' }], + }); + backendApp.DBGetTables.mockImplementation(async (_config: any, dbName: string) => { + if (dbName === 'mkefu_ai_dev') { + return { success: true, data: [{ Tables_in_mkefu_ai_dev: 'ai_conversation' }] }; + } + if (dbName === 'mkefu_dev') { + return { success: true, data: [{ Tables_in_mkefu_dev: 'wechat_visitor_id_bak' }] }; + } + return { success: true, data: [] }; + }); + backendApp.DBGetAllColumns.mockResolvedValue({ success: true, data: [] }); + + await act(async () => { + renderer = create(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + const sqlProvider = editorState.providers.find((provider) => Array.isArray(provider.triggerCharacters) && provider.triggerCharacters.includes('.')); + expect(sqlProvider).toBeTruthy(); + + editorState.value = 'SELECT * FROM wechat'; + editorState.latestOnChange?.(editorState.value); + const result = await sqlProvider.provideCompletionItems(editorState.editor.getModel(), { lineNumber: 1, column: editorState.value.length + 1 }); + const labels = result.suggestions.map((item: any) => item.label); + + expect(labels).not.toContain('wechat_visitor_id_bak'); + expect(labels).not.toContain('mkefu_dev.wechat_visitor_id_bak'); + expect(backendApp.DBGetTables.mock.calls.map((call: any[]) => call[1])).toEqual( + expect.arrayContaining(['mkefu_ai_dev']), + ); + expect(backendApp.DBGetTables.mock.calls.map((call: any[]) => call[1])).not.toContain('mkefu_dev'); + + await act(async () => { + renderer.unmount(); + }); + }); + it('lazy loads current database tables for FROM completion when metadata is not preloaded', async () => { let renderer!: ReactTestRenderer; autoFetchState.visible = false; @@ -1497,6 +1545,12 @@ describe('QueryEditor external SQL save', () => { success: true, data: [{ Table: 'fs_org_auth_application' }], }); + backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => { + if (/table_comment|information_schema\.tables/i.test(sql)) { + return { success: true, data: [{ table_name: 'fs_org_auth_application', table_comment: '认证申请表' }] }; + } + return { success: true, data: [] }; + }); await act(async () => { renderer = create(); @@ -1509,9 +1563,11 @@ describe('QueryEditor external SQL save', () => { editorState.latestOnChange?.(editorState.value); const result = await sqlProvider.provideCompletionItems(editorState.editor.getModel(), { lineNumber: 1, column: editorState.value.length + 1 }); const labels = result.suggestions.map((item: any) => item.label); + const tableSuggestion = result.suggestions.find((item: any) => item.label === 'fs_org_auth_application'); expect(backendApp.DBGetTables).toHaveBeenCalledWith(expect.any(Object), 'front_end_sys'); expect(labels).toContain('fs_org_auth_application'); + expect(tableSuggestion?.detail).toBe('表 - 认证申请表'); await act(async () => { renderer.unmount(); }); @@ -4520,13 +4576,26 @@ describe('QueryEditor external SQL save', () => { data: [{ Tables_in_main: 'users' }, { Tables_in_main: 'reporting.events' }], }); backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] }); + backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => { + if (/table_comment|information_schema\.tables/i.test(sql)) { + return { + success: true, + data: [ + { table_name: 'users', table_comment: '用户表' }, + { table_name: 'reporting.events', table_comment: '事件表' }, + ], + }; + } + return { success: true, data: [] }; + }); await act(async () => { create(); }); await act(async () => { - await Promise.resolve(); - await Promise.resolve(); + for (let i = 0; i < 6; i += 1) { + await Promise.resolve(); + } }); const completionProvider = editorState.providers[0]; @@ -4543,7 +4612,7 @@ describe('QueryEditor external SQL save', () => { expect(tableSuggestion.detail).not.toContain('Table (main.reporting)'); }); - it('localizes global cross-db table completion detail in zh-CN while preserving the raw database name', async () => { + it('keeps database-qualified table completion from leaking into unqualified FROM suggestions', async () => { storeState.languagePreference = 'zh-CN'; setCurrentLanguage('zh-CN'); editorState.value = 'select * from '; @@ -4568,10 +4637,15 @@ describe('QueryEditor external SQL save', () => { expect(completionProvider).toBeTruthy(); editorState.value = 'select * from analytics.'; - await completionProvider.provideCompletionItems( + const qualifiedCompletionItems = await completionProvider.provideCompletionItems( editorState.editor.getModel(), { lineNumber: 1, column: editorState.value.length + 1 }, ); + const qualifiedTableSuggestion = qualifiedCompletionItems?.suggestions?.find((item: any) => item?.label === 'events'); + + expect(qualifiedTableSuggestion).toBeTruthy(); + expect(qualifiedTableSuggestion.detail).toContain('表 (analytics)'); + expect(qualifiedTableSuggestion.detail).not.toContain('Table (analytics)'); editorState.value = 'select * from '; const completionItems = await completionProvider.provideCompletionItems( @@ -4580,9 +4654,7 @@ describe('QueryEditor external SQL save', () => { ); const tableSuggestion = completionItems?.suggestions?.find((item: any) => item?.label === 'analytics.events'); - expect(tableSuggestion).toBeTruthy(); - expect(tableSuggestion.detail).toContain('表 (analytics)'); - expect(tableSuggestion.detail).not.toContain('Table (analytics)'); + expect(tableSuggestion).toBeFalsy(); }); it('localizes current-db table completion detail in zh-CN for plain and schema-qualified tables', async () => { @@ -4596,6 +4668,18 @@ describe('QueryEditor external SQL save', () => { data: [{ Tables_in_main: 'users' }, { Tables_in_main: 'reporting.events' }], }); backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] }); + backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => { + if (/table_comment|information_schema\.tables/i.test(sql)) { + return { + success: true, + data: [ + { table_name: 'users', table_comment: '用户表' }, + { table_name: 'reporting.events', table_comment: '事件表' }, + ], + }; + } + return { success: true, data: [] }; + }); await act(async () => { create(); @@ -4616,11 +4700,11 @@ describe('QueryEditor external SQL save', () => { const schemaTableSuggestion = completionItems?.suggestions?.find((item: any) => item?.label === 'events'); expect(plainTableSuggestion).toBeTruthy(); - expect(plainTableSuggestion.detail).toBe('表'); + expect(plainTableSuggestion.detail).toBe('表 - 用户表'); expect(plainTableSuggestion.detail).not.toContain('Table'); expect(schemaTableSuggestion).toBeTruthy(); - expect(schemaTableSuggestion.detail).toContain('表 (reporting)'); + expect(schemaTableSuggestion.detail).toBe('表 (reporting) - 事件表'); expect(schemaTableSuggestion.detail).not.toContain('Table (reporting)'); }); @@ -4689,9 +4773,44 @@ describe('QueryEditor external SQL save', () => { const idSuggestion = completionItems?.suggestions?.find((item: any) => item?.label === 'id'); expect(idSuggestion).toBeTruthy(); - expect(idSuggestion.documentation).toBe('Comment: 主键ID'); + expect(idSuggestion.documentation).toContain('Comment: 主键ID'); expect(idSuggestion.documentation).not.toBe('备注:主键ID'); }); + + it('shows column type table and comment in SQL completion metadata', async () => { + editorState.value = 'select * from users where u'; + autoFetchState.visible = true; + backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] }); + backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] }); + backendApp.DBGetAllColumns.mockResolvedValueOnce({ + success: true, + data: [{ tableName: 'users', name: 'user_id', type: 'varchar(32)', comment: '用户ID' }], + }); + + await act(async () => { + create(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const completionProvider = editorState.providers[0]; + expect(completionProvider).toBeTruthy(); + + const completionItems = await completionProvider.provideCompletionItems( + editorState.editor.getModel(), + { lineNumber: 1, column: editorState.value.length + 1 }, + ); + const columnSuggestion = completionItems?.suggestions?.find((item: any) => item?.label === 'user_id'); + + expect(columnSuggestion).toBeTruthy(); + expect(columnSuggestion.detail).toBe('users [varchar(32)] - 用户ID'); + expect(columnSuggestion.documentation).toContain('类型: varchar(32)'); + expect(columnSuggestion.documentation).toContain('库: main'); + expect(columnSuggestion.documentation).toContain('表: users'); + expect(columnSuggestion.documentation).toContain('备注:用户ID'); + }); }); it('registers SQL metadata hover provider only once across query editor instances', async () => { @@ -8905,6 +9024,16 @@ describe('QueryEditor external SQL save', () => { expect(appCss).toContain('min-height: 260px;'); expect(appCss).toContain('.gn-query-monaco-stage .monaco-editor .suggest-details {'); expect(appCss).toContain('min-height: 260px;'); + expect(appCss).toContain('.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main {'); + expect(appCss).toContain('justify-content: flex-start;'); + expect(appCss).toContain('gap: 6px;'); + expect(appCss).toContain('.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left {'); + expect(appCss).toContain('flex: 0 1 auto;'); + expect(appCss).toContain('.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right {'); + expect(appCss).toContain('flex: 1 1 auto;'); + expect(appCss).toContain('.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .right > .details-label {'); + expect(appCss).toContain('display: inline !important;'); + expect(appCss).toContain('margin-left: 0;'); expect(appCss).not.toContain('.gn-query-monaco-stage .monaco-editor .suggest-widget {'); expect(appCss).not.toContain('width: 680px;'); expect(appCss).not.toContain('min-width: 560px;');