diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx index 2eb2348d..3aa74dd7 100644 --- a/frontend/src/components/DataGrid.ddl.test.tsx +++ b/frontend/src/components/DataGrid.ddl.test.tsx @@ -4414,6 +4414,102 @@ describe('DataGrid DDL interactions', () => { expect(content).not.toContain('CREATE TABLE users'); }); + it('returns a query result to data preview when a fresh table view request arrives', async () => { + storeState.appearance.uiVersion = 'v2'; + backendApp.DBShowCreateTable + .mockResolvedValueOnce({ + success: true, + data: 'CREATE TABLE users (`id` bigint)', + }) + .mockResolvedValueOnce({ + success: true, + data: 'CREATE TABLE users (`id` bigint)', + }) + .mockResolvedValueOnce({ + success: true, + data: 'CREATE TABLE orders (`id` bigint)', + }); + + const renderGrid = (initialViewModeRequestId?: string, rowId = 1) => ( + + ); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(renderGrid()); + }); + await waitForEffects(); + + await act(async () => { + findButton(renderer!, '查看 DDL').props.onClick(); + }); + await waitForEffects(); + expect(renderer!.root.findAll((node) => node.props['data-grid-ddl-view'])).toHaveLength(1); + + await act(async () => { + renderer!.update(renderGrid('query-run-1', 2)); + }); + await waitForEffects(); + + expect(renderer!.root.findAll((node) => node.props['data-grid-ddl-view'])).toHaveLength(0); + expect(testRenderState.latestTableProps.dataSource[0]).toMatchObject({ id: 2 }); + expect(backendApp.DBShowCreateTable).toHaveBeenCalledTimes(1); + + await act(async () => { + findButton(renderer!, '查看 DDL').props.onClick(); + }); + await waitForEffects(); + expect(renderer!.root.findAll((node) => node.props['data-grid-ddl-view'])).toHaveLength(1); + expect(backendApp.DBShowCreateTable).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ type: 'mysql' }), + 'main', + 'users', + ); + + await act(async () => { + renderer!.update(renderGrid('query-run-2', 3)); + }); + await waitForEffects(); + expect(renderer!.root.findAll((node) => node.props['data-grid-ddl-view'])).toHaveLength(0); + expect(testRenderState.latestTableProps.dataSource[0]).toMatchObject({ id: 3 }); + + await act(async () => { + renderer!.update( + , + ); + }); + await waitForEffects(); + + expect(renderer!.root.findAll((node) => node.props['data-grid-ddl-view'])).toHaveLength(1); + expect(backendApp.DBShowCreateTable).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ type: 'mysql' }), + 'main', + 'orders', + ); + expect(textContent(renderer!.root)).toContain('CREATE TABLE orders'); + expect(backendApp.DBShowCreateTable).toHaveBeenCalledTimes(3); + }); + it('keeps the v2 DDL sidebar open when switching to another table tab instance', async () => { storeState.appearance.uiVersion = 'v2'; let resolveOrdersRequest: (value: any) => void = () => {}; diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 182ef2bb..9cb0e0e5 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -309,6 +309,7 @@ const DataGrid: React.FC = ({ scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions, showRowNumberColumn, isActive = true, enableSqlLogEvent = false, initialViewMode, initialViewModeRequestId, + initialViewModeScope, onDataViewActivate, onDataChange, }) => { @@ -1749,6 +1750,7 @@ const DataGrid: React.FC = ({ translate: translateDataGrid, initialViewMode, initialViewModeRequestId, + initialViewModeScope, }); useEffect(() => { diff --git a/frontend/src/components/DataGridCore.tsx b/frontend/src/components/DataGridCore.tsx index d9106437..9c10623f 100644 --- a/frontend/src/components/DataGridCore.tsx +++ b/frontend/src/components/DataGridCore.tsx @@ -1375,6 +1375,7 @@ interface DataGridProps { enableSqlLogEvent?: boolean; initialViewMode?: GridViewMode; initialViewModeRequestId?: string; + initialViewModeScope?: 'shared' | 'local'; onDataViewActivate?: () => void; onDataChange?: (rows: any[]) => void; } diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index afef0ebe..5aa7287d 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -2333,8 +2333,8 @@ describe('QueryEditor external SQL save', () => { renderer.unmount(); }); - it.each(['sqlite', 'clickhouse'])( - 'activates the data result tab for %s after the sql log tab was open', + it.each(['sqlite', 'clickhouse', 'mongodb'])( + 'activates the data result tab and requests data preview for %s after the sql log tab was open', async (dbType) => { storeState.appearance.uiVersion = 'v2'; storeState.connections[0].config.type = dbType; @@ -2350,14 +2350,22 @@ describe('QueryEditor external SQL save', () => { data: [{ name: 'id', key: 'PRI' }], }); backendApp.DBGetIndexes.mockResolvedValue({ success: true, data: [] }); - backendApp.DBQueryMulti.mockResolvedValueOnce({ - success: true, - data: [{ - columns: ['id', 'name'], - rows: [{ id: 1, name: 'alpha' }], - statementIndex: 1, - }], - }); + if (dbType === 'mongodb') { + backendApp.DBQueryWithCancel.mockResolvedValue({ + success: true, + data: [{ id: 1, name: 'alpha' }], + fields: ['id', 'name'], + }); + } else { + backendApp.DBQueryMulti.mockResolvedValue({ + success: true, + data: [{ + columns: ['id', 'name'], + rows: [{ id: 1, name: 'alpha' }], + statementIndex: 1, + }], + }); + } const windowListeners: Record void)[]> = {}; vi.stubGlobal('window', { @@ -2400,8 +2408,56 @@ describe('QueryEditor external SQL save', () => { expect(textContent(renderer.toJSON())).toContain('结果 1'); expect(dataGridState.latestProps?.columnNames).toEqual(['id', 'name']); expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ id: 1, name: 'alpha' }); + expect(dataGridState.latestProps?.initialViewMode).toBe('table'); + expect(dataGridState.latestProps?.initialViewModeScope).toBe('local'); + const firstDataPreviewRequestId = dataGridState.latestProps?.initialViewModeRequestId; + expect(firstDataPreviewRequestId).toEqual(expect.any(String)); - renderer.unmount(); + await act(async () => { + await findButton(renderer, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(dataGridState.latestProps?.initialViewMode).toBe('table'); + expect(dataGridState.latestProps?.initialViewModeScope).toBe('local'); + expect(dataGridState.latestProps?.initialViewModeRequestId).toEqual(expect.any(String)); + expect(dataGridState.latestProps?.initialViewModeRequestId).not.toBe(firstDataPreviewRequestId); + + const secondDataPreviewRequestId = dataGridState.latestProps?.initialViewModeRequestId; + if (dbType === 'mongodb') { + backendApp.DBQueryWithCancel.mockResolvedValueOnce({ + success: true, + data: [], + fields: [], + }); + } else { + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ columns: [], rows: [], statementIndex: 1 }], + }); + } + + await act(async () => { + await findButton(renderer, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(dataGridState.latestProps?.columnNames).toEqual([]); + expect(dataGridState.latestProps?.data).toEqual([]); + expect(dataGridState.latestProps?.initialViewMode).toBe('table'); + expect(dataGridState.latestProps?.initialViewModeScope).toBe('local'); + expect(dataGridState.latestProps?.initialViewModeRequestId).toEqual(expect.any(String)); + expect(dataGridState.latestProps?.initialViewModeRequestId).not.toBe(secondDataPreviewRequestId); + + await act(async () => { + renderer.unmount(); + }); }, ); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 2ea8528b..0d952174 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -1285,6 +1285,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const [activeResultKey, setActiveResultKey] = useState( () => restoredResultSessionRef.current?.activeResultKey || '', ); + const [resultDataPreviewRequest, setResultDataPreviewRequest] = useState<{ + resultKey: string; + requestId: string; + } | null>(null); const resultSetsRef = useRef(resultSets); const activeResultKeyRef = useRef(activeResultKey); const nativeRestoredResultRefs = useRef(new Map< @@ -6355,6 +6359,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc ), ); + const isQueryDataGridResultSet = (result?: ResultSet | null): boolean => + Boolean( + result && + result.resultType !== 'message' && + !isAffectedRowsResultSet(result), + ); + const resolveActiveResultKeyAfterMerge = (merged: ResultSet[], executed: ResultSet[]): string => { const firstExecutedResult = executed.find((result) => isConcreteGridResultSet(result)) || executed.find((result) => isMessageLikeResultSet(result)) @@ -6371,6 +6382,18 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc || ''; }; + const activateExecutedResult = (merged: ResultSet[], executed: ResultSet[], requestSeq: number) => { + const nextActiveResultKey = resolveActiveResultKeyAfterMerge(merged, executed); + const nextActiveResult = merged.find((result) => result.key === nextActiveResultKey); + setActiveResultKey(nextActiveResultKey); + setResultDataPreviewRequest(isQueryDataGridResultSet(nextActiveResult) + ? { + resultKey: nextActiveResultKey, + requestId: `${tab.id}:${requestSeq}`, + } + : null); + }; + const resolveExecutableSQLAtEditorPosition = (model: any, sqlText: string, position: any, dbType = ''): string => { const normalizedPosition = normalizeEditorPosition(position); if (!normalizedPosition) return ''; @@ -7085,7 +7108,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const shouldReplaceAllResults = didExecuteWholeEditor; const mergedResultSets = mergeResultSets(resultSets, nextResultSets, shouldReplaceAllResults); setResultSets(mergedResultSets); - setActiveResultKey(resolveActiveResultKeyAfterMerge(mergedResultSets, nextResultSets)); + activateExecutedResult(mergedResultSets, nextResultSets, runSeq); if (didExecuteAppendedSql || didExecuteWholeEditor) { lastExecutedEditorQueryRef.current = currentQuery; } @@ -7496,7 +7519,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const shouldReplaceAllResults = didExecuteWholeEditor; const mergedResultSets = mergeResultSets(resultSets, nextResultSets, shouldReplaceAllResults); setResultSets(mergedResultSets); - setActiveResultKey(resolveActiveResultKeyAfterMerge(mergedResultSets, nextResultSets)); + activateExecutedResult(mergedResultSets, nextResultSets, runSeq); if (didExecuteAppendedSql || didExecuteWholeEditor) { lastExecutedEditorQueryRef.current = currentQuery; } @@ -9079,6 +9102,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc isV2Ui={isV2Ui} currentDb={currentDb} currentConnectionId={currentConnectionId} + dataPreviewRequest={resultDataPreviewRequest} toggleShortcutLabel={toggleQueryResultsPanelShortcutLabel} onActiveResultKeyChange={setActiveResultKey} onHide={() => updateResultPanelVisibility(false)} diff --git a/frontend/src/components/QueryEditorResultsPanel.tsx b/frontend/src/components/QueryEditorResultsPanel.tsx index 55061fb4..7ef50083 100644 --- a/frontend/src/components/QueryEditorResultsPanel.tsx +++ b/frontend/src/components/QueryEditorResultsPanel.tsx @@ -79,6 +79,7 @@ interface QueryEditorResultsPanelProps { isV2Ui: boolean; currentDb: string; currentConnectionId: string; + dataPreviewRequest?: { resultKey: string; requestId: string } | null; toggleShortcutLabel: string; onActiveResultKeyChange: (key: string) => void; onHide: () => void; @@ -140,6 +141,7 @@ const QueryEditorResultsPanel: React.FC = ({ isV2Ui, currentDb, currentConnectionId, + dataPreviewRequest, toggleShortcutLabel, onActiveResultKeyChange, onHide, @@ -601,6 +603,9 @@ const QueryEditorResultsPanel: React.FC = ({ ddlDbName={rs.ddlDbName} ddlTableName={rs.ddlTableName} connectionId={currentConnectionId} + initialViewMode={dataPreviewRequest?.resultKey === rs.key ? 'table' : undefined} + initialViewModeRequestId={dataPreviewRequest?.resultKey === rs.key ? dataPreviewRequest.requestId : undefined} + initialViewModeScope={dataPreviewRequest?.resultKey === rs.key ? 'local' : undefined} pkColumns={rs.pkColumns} editLocator={rs.editLocator} onReload={() => { diff --git a/frontend/src/components/useDataGridDdlView.ts b/frontend/src/components/useDataGridDdlView.ts index 4f1f35e1..3190d69d 100644 --- a/frontend/src/components/useDataGridDdlView.ts +++ b/frontend/src/components/useDataGridDdlView.ts @@ -105,6 +105,7 @@ interface UseDataGridDdlViewParams { translate?: (key: string, params?: TranslateParams) => string; initialViewMode?: GridViewMode; initialViewModeRequestId?: string; + initialViewModeScope?: 'shared' | 'local'; } export interface UseDataGridDdlViewResult { @@ -145,10 +146,19 @@ export const useDataGridDdlView = ({ translate, initialViewMode, initialViewModeRequestId, + initialViewModeScope, }: UseDataGridDdlViewParams): UseDataGridDdlViewResult => { - const canRestoreSharedDdlView = isV2Ui && canViewDdl && !!currentConnConfig && !!tableName && shouldRestoreSharedDdlView(); - const shouldStartWithSharedDdlView = isActive && canRestoreSharedDdlView; const initialResolvedViewMode = resolveInitialGridViewMode(initialViewMode, isV2Ui, canViewDdl); + const shouldInitiallySuppressSharedDdlView = initialViewModeScope === 'local' + && initialResolvedViewMode === 'table'; + const [suppressSharedDdlView, setSuppressSharedDdlView] = React.useState(shouldInitiallySuppressSharedDdlView); + const canRestoreSharedDdlView = isV2Ui + && canViewDdl + && !!currentConnConfig + && !!tableName + && !suppressSharedDdlView + && shouldRestoreSharedDdlView(); + const shouldStartWithSharedDdlView = isActive && canRestoreSharedDdlView; const [viewMode, setViewMode] = React.useState(() => ( shouldStartWithSharedDdlView ? 'ddl' : (initialResolvedViewMode || 'table') )); @@ -174,13 +184,21 @@ export const useDataGridDdlView = ({ () => buildDdlContextKey(currentConnConfig, dbName, tableName), [currentConnConfig, dbName, tableName], ); + const appliedInitialViewModeKeyRef = React.useRef(''); + const initialViewModeApplyKey = `${ddlContextKey}\u0001${initialResolvedViewMode || ''}\u0001${initialViewModeRequestId || ''}\u0001${initialViewModeScope || 'shared'}`; + const hasPendingLocalTableViewRequest = isActive + && initialResolvedViewMode === 'table' + && initialViewModeScope === 'local' + && appliedInitialViewModeKeyRef.current !== initialViewModeApplyKey; const ddlViewLayout = ddlViewLayoutState; - const resolvedViewMode: GridViewMode = isActive - && canRestoreSharedDdlView - && (viewMode === 'table' || viewMode === 'ddl') - ? 'ddl' - : viewMode; + const resolvedViewMode: GridViewMode = hasPendingLocalTableViewRequest + ? 'table' + : isActive + && canRestoreSharedDdlView + && (viewMode === 'table' || viewMode === 'ddl') + ? 'ddl' + : viewMode; const isDdlContextPending = resolvedViewMode === 'ddl' && isActive && canRestoreSharedDdlView @@ -223,6 +241,7 @@ export const useDataGridDdlView = ({ const requestSeq = ++ddlRequestSeqRef.current; ddlRequestedContextKeyRef.current = ddlContextKey; if (asView) { + setSuppressSharedDdlView(false); setSharedDdlViewOpen(true); setViewMode('ddl'); setDdlModalOpen(false); @@ -255,6 +274,7 @@ export const useDataGridDdlView = ({ }, [isV2Ui, viewMode]); const closeDdlView = React.useCallback(() => { + setSuppressSharedDdlView(false); setSharedDdlViewOpen(false); ddlRequestedContextKeyRef.current = null; ddlRequestSeqRef.current += 1; @@ -266,13 +286,13 @@ export const useDataGridDdlView = ({ }, []); React.useEffect(() => { - if (!isActive || !isV2Ui || !shouldRestoreSharedDdlView()) return; - if (!canViewDdl || !currentConnConfig || !tableName) return; + if (!isActive || hasPendingLocalTableViewRequest || !canRestoreSharedDdlView) return; if (ddlRequestedContextKeyRef.current === ddlContextKey) return; void handleOpenTableDdl({ asView: true }); - }, [canViewDdl, currentConnConfig, ddlContextKey, handleOpenTableDdl, isActive, isV2Ui, tableName]); + }, [canRestoreSharedDdlView, ddlContextKey, handleOpenTableDdl, hasPendingLocalTableViewRequest, isActive]); const handleViewModeChange = React.useCallback((nextMode: GridViewMode) => { + setSuppressSharedDdlView(false); if ((nextMode === 'fields' || nextMode === 'ddl' || nextMode === 'er' || nextMode === 'sqlLog') && !isV2Ui) { setSharedDdlViewOpen(false); setViewMode('table'); @@ -309,16 +329,18 @@ export const useDataGridDdlView = ({ setViewMode(nextMode); }, [cellEditMode, closeCellEditModeRef, closeDdlView, handleOpenTableDdl, isV2Ui, mergedDisplayDataRef, resolvedViewMode, rowKeyStr, selectedRowKeys, setTextRecordIndex]); - const appliedInitialViewModeKeyRef = React.useRef(''); React.useEffect(() => { if (!isActive) return; - const nextInitialViewMode = resolveInitialGridViewMode(initialViewMode, isV2Ui, canViewDdl); - if (!nextInitialViewMode) return; - const applyKey = `${ddlContextKey}\u0001${nextInitialViewMode}\u0001${initialViewModeRequestId || ''}`; - if (appliedInitialViewModeKeyRef.current === applyKey) return; - appliedInitialViewModeKeyRef.current = applyKey; - handleViewModeChange(nextInitialViewMode); - }, [canViewDdl, ddlContextKey, handleViewModeChange, initialViewMode, initialViewModeRequestId, isActive, isV2Ui]); + if (!initialResolvedViewMode) return; + if (appliedInitialViewModeKeyRef.current === initialViewModeApplyKey) return; + appliedInitialViewModeKeyRef.current = initialViewModeApplyKey; + if (initialResolvedViewMode === 'table' && initialViewModeScope === 'local') { + setSuppressSharedDdlView(true); + setViewMode('table'); + return; + } + handleViewModeChange(initialResolvedViewMode); + }, [handleViewModeChange, initialResolvedViewMode, initialViewModeApplyKey, initialViewModeScope, isActive]); const handleDdlSidebarResizeStart = React.useCallback((event: React.MouseEvent) => { event.preventDefault();