From 293fc6e0fe687b0e8b40f5cd899e90bd4182fc7f Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 18 Jun 2026 10:57:51 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(data-grid):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AD=97=E6=AE=B5=E5=85=83=E6=95=B0=E6=8D=AE=E5=81=B6?= =?UTF-8?q?=E5=8F=91=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为字段元数据提取补充可用性判断并在空类型/空备注时自动重试 - 刷新结果集时同步清理字段、外键和唯一键缓存并强制补拉元数据 - 补充 DataGrid 头部元数据回归测试,覆盖首次空结果重试与刷新重载场景 --- frontend/src/components/DataGrid.ddl.test.tsx | 107 ++++++++++++++++++ frontend/src/components/DataGrid.tsx | 85 ++++++++++---- 2 files changed, 169 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx index fb0d4b8..e42380d 100644 --- a/frontend/src/components/DataGrid.ddl.test.tsx +++ b/frontend/src/components/DataGrid.ddl.test.tsx @@ -9,6 +9,7 @@ import DataGrid, { GONAVI_ROW_KEY, hasDataGridVirtualEditRenderVersionChanged, } from './DataGrid'; +import DataGridToolbarFrame from './DataGridToolbarFrame'; import { V2CellContextMenuView, V2ColumnHeaderContextMenuView, V2TableGroupContextMenuView } from './V2TableContextMenu'; import { setCurrentLanguage, t } from '../i18n'; import { DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator'; @@ -96,6 +97,9 @@ vi.mock('../store', () => ({ })); vi.mock('../../wailsjs/go/app/App', () => backendApp); +vi.mock('../../wailsjs/runtime/runtime', () => ({ + EventsOn: vi.fn(() => vi.fn()), +})); vi.mock('react-dom', async () => { const actual = await vi.importActual('react-dom'); @@ -291,6 +295,9 @@ vi.mock('antd', () => { const Radio: any = ({ children }: any) => {children}; Radio.Group = ({ children }: any) =>
{children}
; Radio.Button = ({ children }: any) => ; + const Typography: any = ({ children }: any) => <>{children}; + Typography.Text = ({ children }: any) => {children}; + Typography.Paragraph = ({ children }: any) =>

{children}

; const Segmented = ({ value, options, onChange }: any) => (
{(options || []).map((option: any) => ( @@ -337,6 +344,12 @@ vi.mock('antd', () => { Space, Tag, Radio, + Typography, + Progress: ({ percent, status, format }: any) => ( +
+ {typeof format === 'function' ? format(percent) : null} +
+ ), }; }); @@ -348,6 +361,15 @@ const textContent = (node: any): string => const findButton = (renderer: ReactTestRenderer, text: string) => renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0]; +const renderHeaderText = (columnKey: string): string => { + const column = testRenderState.latestColumns.find((item) => item.key === columnKey); + expect(column).toBeTruthy(); + const headerRenderer = create(<>{column.title}); + const content = textContent(headerRenderer.root); + headerRenderer.unmount(); + return content; +}; + const waitForEffects = async () => { await act(async () => { await Promise.resolve(); @@ -651,6 +673,8 @@ describe('DataGrid DDL interactions', () => { backendApp.DBQuery.mockResolvedValue({ success: true, data: [] }); backendApp.DBShowCreateTable.mockResolvedValue({ success: true, data: 'CREATE TABLE users' }); setCurrentLanguage('zh-CN'); + storeState.queryOptions.showColumnComment = false; + storeState.queryOptions.showColumnType = false; storeState.appearance.uiVersion = 'legacy'; storeState.dataEditTransactionOptions = { commitMode: 'manual', @@ -733,6 +757,7 @@ describe('DataGrid DDL interactions', () => { dbName="main" connectionId="conn-1" pkColumns={['id']} + onReload={() => {}} />, ); }); @@ -866,6 +891,88 @@ describe('DataGrid DDL interactions', () => { renderer!.unmount(); }); + it('retries column metadata loading when the first response has no usable type or comment', async () => { + storeState.queryOptions.showColumnComment = true; + storeState.queryOptions.showColumnType = true; + backendApp.DBGetColumns + .mockResolvedValueOnce({ + success: true, + data: [{ Name: 'id', Type: '', Comment: '' }], + }) + .mockResolvedValueOnce({ + success: true, + data: [{ Name: 'id', Type: 'bigint', Comment: '主键 ID' }], + }); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + , + ); + }); + await waitForEffects(); + await waitForEffects(); + + expect(backendApp.DBGetColumns).toHaveBeenCalledTimes(2); + const headerText = renderHeaderText('id'); + expect(headerText).toContain('bigint'); + expect(headerText).toContain('主键 ID'); + renderer!.unmount(); + }); + + it('reloads column metadata after clicking refresh', async () => { + storeState.queryOptions.showColumnComment = true; + storeState.queryOptions.showColumnType = true; + backendApp.DBGetColumns + .mockResolvedValueOnce({ + success: true, + data: [{ Name: 'id', Type: 'bigint', Comment: '旧备注' }], + }) + .mockResolvedValueOnce({ + success: true, + data: [{ Name: 'id', Type: 'varchar(64)', Comment: '新备注' }], + }); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + , + ); + }); + await waitForEffects(); + + expect(backendApp.DBGetColumns).toHaveBeenCalledTimes(1); + expect(renderHeaderText('id')).toContain('旧备注'); + + await act(async () => { + renderer!.root.findByType(DataGridToolbarFrame).props.onRefresh(); + }); + await waitForEffects(); + await waitForEffects(); + + expect(backendApp.DBGetColumns).toHaveBeenCalledTimes(2); + const headerText = renderHeaderText('id'); + expect(headerText).toContain('varchar(64)'); + expect(headerText).toContain('新备注'); + renderer!.unmount(); + }); + it('localizes v2 column header fallback labels', () => { setCurrentLanguage('en-US'); diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index f8e0a18..4ccd740 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -1346,6 +1346,27 @@ type ColumnMeta = { comment: string; }; +const buildColumnMetaMap = (columns: ColumnDefinition[]): Record => { + const nextMap: Record = {}; + (columns || []).forEach((column: any) => { + const name = getColumnDefinitionName(column); + if (!name) return; + nextMap[name] = { + type: getColumnDefinitionType(column), + comment: getColumnDefinitionComment(column), + }; + }); + return nextMap; +}; + +const hasUsableColumnMeta = (metaMap: Record): boolean => ( + Object.values(metaMap || {}).some((meta) => { + const type = String(meta?.type || '').trim(); + const comment = String(meta?.comment || '').trim(); + return type.length > 0 || comment.length > 0; + }) +); + type ForeignKeyTarget = { columnName: string; refTableName: string; @@ -2210,6 +2231,7 @@ const DataGrid: React.FC = ({ const [columnMetaMap, setColumnMetaMap] = useState>({}); const [foreignKeyMap, setForeignKeyMap] = useState>({}); const [uniqueKeyGroups, setUniqueKeyGroups] = useState([]); + const [metadataReloadVersion, setMetadataReloadVersion] = useState(0); const mergedDisplayDataRef = useRef([]); const closeCellEditModeRef = useRef<() => void>(() => {}); const formRef = useRef(form); @@ -2269,29 +2291,37 @@ const DataGrid: React.FC = ({ }; const seq = ++columnMetaSeqRef.current; - DBGetColumns(buildRpcConnectionConfig(config) as any, normalizedDbName, normalizedTableName) - .then((res) => { - if (seq !== columnMetaSeqRef.current) return; - if (!res.success || !Array.isArray(res.data)) { - setColumnMetaMap({}); - return; + const loadColumnMeta = async () => { + let nextMap: Record | null = null; + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + const res = await DBGetColumns(buildRpcConnectionConfig(config) as any, normalizedDbName, normalizedTableName); + if (seq !== columnMetaSeqRef.current) return; + if (!res.success || !Array.isArray(res.data)) { + continue; + } + const candidateMap = buildColumnMetaMap(res.data as ColumnDefinition[]); + if (!hasUsableColumnMeta(candidateMap)) { + continue; + } + nextMap = candidateMap; + break; + } catch { + if (seq !== columnMetaSeqRef.current) return; } - const nextMap: Record = {}; - (res.data as ColumnDefinition[]).forEach((column: any) => { - const name = getColumnDefinitionName(column); - if (!name) return; - const type = getColumnDefinitionType(column); - const comment = getColumnDefinitionComment(column); - nextMap[name] = { type, comment }; - }); + } + + if (seq !== columnMetaSeqRef.current) return; + if (nextMap) { columnMetaCacheRef.current[cacheKey] = nextMap; setColumnMetaMap(nextMap); - }) - .catch(() => { - if (seq !== columnMetaSeqRef.current) return; - setColumnMetaMap({}); - }); - }, [connections, connectionId, dbName, tableName]); + return; + } + setColumnMetaMap({}); + }; + + void loadColumnMeta(); + }, [connections, connectionId, dbName, tableName, metadataReloadVersion]); useEffect(() => { const normalizedTableName = String(tableName || '').trim(); @@ -2344,7 +2374,7 @@ const DataGrid: React.FC = ({ if (seq !== foreignKeySeqRef.current) return; setForeignKeyMap({}); }); - }, [connections, connectionId, dbName, tableName, exportScope]); + }, [connections, connectionId, dbName, tableName, exportScope, metadataReloadVersion]); useEffect(() => { const normalizedTableName = String(tableName || '').trim(); @@ -2385,7 +2415,7 @@ const DataGrid: React.FC = ({ if (seq !== uniqueKeyGroupsSeqRef.current) return; setUniqueKeyGroups([]); }); - }, [connections, connectionId, dbName, tableName]); + }, [connections, connectionId, dbName, tableName, metadataReloadVersion]); const columnMetaMapByLowerName = useMemo(() => { const next: Record = {}; @@ -7741,8 +7771,17 @@ const DataGrid: React.FC = ({ setDeletedRowKeys(new Set()); setModifiedColumns({}); setSelectedRowKeys([]); + const normalizedTableName = String(tableName || '').trim(); + const normalizedDbName = String(dbName || '').trim(); + if (connectionId && normalizedTableName) { + const cacheKey = `${connectionId}|${normalizedDbName}|${normalizedTableName}`; + delete columnMetaCacheRef.current[cacheKey]; + delete foreignKeyCacheRef.current[cacheKey]; + delete uniqueKeyGroupsCacheRef.current[cacheKey]; + setMetadataReloadVersion((value) => value + 1); + } if (onReload) onReload(); - }, [clearAutoCommitTimer, onReload]); + }, [clearAutoCommitTimer, connectionId, dbName, onReload, tableName]); const handleResetPendingChanges = useCallback(() => { clearAutoCommitTimer();