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();