mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-03 07:01:22 +08:00
🐛 fix(data-grid): 修复字段元数据偶发缺失
- 为字段元数据提取补充可用性判断并在空类型/空备注时自动重试 - 刷新结果集时同步清理字段、外键和唯一键缓存并强制补拉元数据 - 补充 DataGrid 头部元数据回归测试,覆盖首次空结果重试与刷新重载场景
This commit is contained in:
@@ -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<any>('react-dom');
|
||||
@@ -291,6 +295,9 @@ vi.mock('antd', () => {
|
||||
const Radio: any = ({ children }: any) => <span>{children}</span>;
|
||||
Radio.Group = ({ children }: any) => <div>{children}</div>;
|
||||
Radio.Button = ({ children }: any) => <button type="button">{children}</button>;
|
||||
const Typography: any = ({ children }: any) => <>{children}</>;
|
||||
Typography.Text = ({ children }: any) => <span>{children}</span>;
|
||||
Typography.Paragraph = ({ children }: any) => <p>{children}</p>;
|
||||
const Segmented = ({ value, options, onChange }: any) => (
|
||||
<div data-segmented-value={value}>
|
||||
{(options || []).map((option: any) => (
|
||||
@@ -337,6 +344,12 @@ vi.mock('antd', () => {
|
||||
Space,
|
||||
Tag,
|
||||
Radio,
|
||||
Typography,
|
||||
Progress: ({ percent, status, format }: any) => (
|
||||
<div data-progress-percent={String(percent)} data-progress-status={String(status)}>
|
||||
{typeof format === 'function' ? format(percent) : null}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<DataGrid
|
||||
data={[{ __gonavi_row_key__: 'row-1', id: 1 }]}
|
||||
columnNames={['id']}
|
||||
loading={false}
|
||||
tableName="users"
|
||||
dbName="main"
|
||||
connectionId="conn-1"
|
||||
pkColumns={['id']}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
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(
|
||||
<DataGrid
|
||||
data={[{ __gonavi_row_key__: 'row-1', id: 1 }]}
|
||||
columnNames={['id']}
|
||||
loading={false}
|
||||
tableName="users"
|
||||
dbName="main"
|
||||
connectionId="conn-1"
|
||||
pkColumns={['id']}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
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');
|
||||
|
||||
|
||||
@@ -1346,6 +1346,27 @@ type ColumnMeta = {
|
||||
comment: string;
|
||||
};
|
||||
|
||||
const buildColumnMetaMap = (columns: ColumnDefinition[]): Record<string, ColumnMeta> => {
|
||||
const nextMap: Record<string, ColumnMeta> = {};
|
||||
(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<string, ColumnMeta>): 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<DataGridProps> = ({
|
||||
const [columnMetaMap, setColumnMetaMap] = useState<Record<string, ColumnMeta>>({});
|
||||
const [foreignKeyMap, setForeignKeyMap] = useState<Record<string, ForeignKeyTarget>>({});
|
||||
const [uniqueKeyGroups, setUniqueKeyGroups] = useState<string[][]>([]);
|
||||
const [metadataReloadVersion, setMetadataReloadVersion] = useState(0);
|
||||
const mergedDisplayDataRef = useRef<Item[]>([]);
|
||||
const closeCellEditModeRef = useRef<() => void>(() => {});
|
||||
const formRef = useRef(form);
|
||||
@@ -2269,29 +2291,37 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
};
|
||||
|
||||
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<string, ColumnMeta> | 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<string, ColumnMeta> = {};
|
||||
(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<DataGridProps> = ({
|
||||
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<DataGridProps> = ({
|
||||
if (seq !== uniqueKeyGroupsSeqRef.current) return;
|
||||
setUniqueKeyGroups([]);
|
||||
});
|
||||
}, [connections, connectionId, dbName, tableName]);
|
||||
}, [connections, connectionId, dbName, tableName, metadataReloadVersion]);
|
||||
|
||||
const columnMetaMapByLowerName = useMemo(() => {
|
||||
const next: Record<string, ColumnMeta> = {};
|
||||
@@ -7741,8 +7771,17 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user