mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-04 17:41:38 +08:00
🐛 fix(query-editor): 修复消息结果前缀与复制全选交互
- 统一清洗 SQL Server 消息前缀并覆盖结果刷新与分页回填链路 - 将消息结果区改为只读文本区,补充一键复制入口 - 放行编辑器外可编辑区域的 cmd/ctrl+a,避免消息内容全选被抢占 - 补充结果面板交互与国际化回归,确保构建通过
This commit is contained in:
@@ -8,6 +8,7 @@ import { setCurrentLanguage } from '../i18n';
|
||||
import type { SavedQuery, TabData } from '../types';
|
||||
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
|
||||
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
|
||||
import { normalizeQueryResultMessages } from './queryEditor/QueryEditorHelpers';
|
||||
import QueryEditor, {
|
||||
collectQueryEditorObjectDecorationCandidates,
|
||||
resolveQueryEditorNavigationDecorations,
|
||||
@@ -780,6 +781,18 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(dataGridState.latestProps?.columnNames).not.toEqual([]);
|
||||
});
|
||||
|
||||
it('normalizes sqlserver mssql-prefixed message lines line-by-line', () => {
|
||||
expect(normalizeQueryResultMessages([
|
||||
"mssql: select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
"mssql: 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
" where funcno = @funcno and tabname = '$vendorclass'",
|
||||
])).toEqual([
|
||||
"select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
"'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
"where funcno = @funcno and tabname = '$vendorclass'",
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps multiple result sets from a single sqlserver statement', async () => {
|
||||
storeState.connections[0].config.type = 'sqlserver';
|
||||
storeState.connections[0].config.database = 'master';
|
||||
@@ -925,6 +938,46 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(dataGridState.latestProps).toBeNull();
|
||||
});
|
||||
|
||||
it('strips mssql prefixes before rendering sqlserver message-only results', async () => {
|
||||
storeState.connections[0].config.type = 'sqlserver';
|
||||
storeState.connections[0].config.database = 'hydee';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
statementIndex: 1,
|
||||
columns: [],
|
||||
rows: [],
|
||||
messages: [
|
||||
"mssql: select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
"mssql: 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
" where funcno = @funcno and tabname = '$vendorclass'",
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ dbName: 'hydee', query: "sp_sql p_get_query" })} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const rendered = textContent(renderer!.toJSON());
|
||||
expect(rendered).toContain('消息 1');
|
||||
expect(rendered).toContain("select c.queryno,'' ,left(dbo.f_vendor_class");
|
||||
expect(rendered).toContain("'char','',''),'自动生成'");
|
||||
expect(rendered).toContain("where funcno = @funcno and tabname = '$vendorclass'");
|
||||
expect(rendered).not.toContain('mssql:');
|
||||
});
|
||||
|
||||
it('renders top-level sqlserver print messages when result sets contain only status rows', async () => {
|
||||
storeState.connections[0].config.type = 'sqlserver';
|
||||
storeState.connections[0].config.database = 'hydee';
|
||||
@@ -2335,6 +2388,24 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-text {');
|
||||
});
|
||||
|
||||
it('keeps query message blocks explicitly left, top aligned, copyable, and textarea-based', () => {
|
||||
const source = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).toContain("textAlign: 'left'");
|
||||
expect(source).toContain("justifyContent: 'flex-start'");
|
||||
expect(source).toContain("data-query-result-message-textarea");
|
||||
expect(source).toContain("query_editor.results_panel.message.action.copy");
|
||||
expect(source).toContain("typeof navigator?.clipboard?.writeText !== 'function'");
|
||||
expect(source).toContain('await navigator.clipboard.writeText(safeText);');
|
||||
expect(source).toContain('event.currentTarget.select();');
|
||||
});
|
||||
|
||||
it('keeps editor select-all scoped away from non-editor editable targets', () => {
|
||||
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).toContain("if (isEditableElement(event.target) && !inEditorPane) {");
|
||||
});
|
||||
|
||||
it('embeds the sql execution log as a result tab instead of a standalone workspace panel in v2', () => {
|
||||
const panelSource = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8');
|
||||
const editorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
|
||||
|
||||
@@ -95,6 +95,7 @@ import {
|
||||
isDocumentLevelShortcutTarget,
|
||||
isQueryEditorPrimaryMouseButton,
|
||||
normalizeCommentText,
|
||||
normalizeQueryResultMessages,
|
||||
normalizeCompletionQualifiedName,
|
||||
normalizeEditorPosition,
|
||||
normalizeExecutedSqlKey,
|
||||
@@ -2832,6 +2833,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const cols = (rsData.columns && rsData.columns.length > 0)
|
||||
? rsData.columns
|
||||
: (rows.length > 0 ? Object.keys(rows[0]) : []);
|
||||
const refreshedMessages = normalizeQueryResultMessages(rsData?.messages);
|
||||
rows.forEach((row: any, i: number) => {
|
||||
if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = i;
|
||||
});
|
||||
@@ -2843,8 +2845,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
...rs,
|
||||
rows,
|
||||
columns: cols,
|
||||
messages: Array.isArray(rsData.messages) ? rsData.messages : [],
|
||||
resultType: ((!Array.isArray(rsData.rows) || rsData.rows.length === 0) && (!Array.isArray(rsData.columns) || rsData.columns.length === 0) && Array.isArray(rsData.messages) && rsData.messages.length > 0)
|
||||
messages: refreshedMessages,
|
||||
resultType: ((!Array.isArray(rsData.rows) || rsData.rows.length === 0) && (!Array.isArray(rsData.columns) || rsData.columns.length === 0) && refreshedMessages.length > 0)
|
||||
? 'message'
|
||||
: 'grid',
|
||||
truncated,
|
||||
@@ -2926,6 +2928,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const cols = (rsData.columns && rsData.columns.length > 0)
|
||||
? rsData.columns
|
||||
: (rows.length > 0 ? Object.keys(rows[0]) : target.columns);
|
||||
const pageMessages = normalizeQueryResultMessages(rsData?.messages);
|
||||
const totalState = resolveQueryResultPaginationTotal({
|
||||
current: safePage,
|
||||
pageSize: safePageSize,
|
||||
@@ -2938,7 +2941,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
...rs,
|
||||
rows,
|
||||
columns: cols,
|
||||
messages: Array.isArray(rsData.messages) ? rsData.messages : [],
|
||||
messages: pageMessages,
|
||||
resultType: 'grid',
|
||||
truncated: false,
|
||||
page: {
|
||||
@@ -3091,6 +3094,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
setQueryId(queryId);
|
||||
|
||||
const res = await DBQueryWithCancel(buildRpcConnectionConfig(config) as any, currentDb, executedSql, queryId);
|
||||
const legacyResultMessages = normalizeQueryResultMessages(res?.messages);
|
||||
const duration = Date.now() - startTime;
|
||||
addSqlLog({
|
||||
id: `log-${Date.now()}-query-${idx + 1}`,
|
||||
@@ -3134,12 +3138,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
statementResultIndex: 1,
|
||||
rows,
|
||||
columns: cols,
|
||||
messages: Array.isArray(res.messages) ? res.messages : [],
|
||||
messages: legacyResultMessages,
|
||||
pkColumns: [],
|
||||
readOnly: true,
|
||||
truncated
|
||||
});
|
||||
} else if (Array.isArray(res.messages) && res.messages.length > 0) {
|
||||
} else if (legacyResultMessages.length > 0) {
|
||||
nextResultSets.push({
|
||||
key: `result-${idx + 1}`,
|
||||
sql: rawStatement,
|
||||
@@ -3148,7 +3152,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
statementResultIndex: 1,
|
||||
rows: [],
|
||||
columns: [],
|
||||
messages: res.messages,
|
||||
messages: legacyResultMessages,
|
||||
resultType: 'message',
|
||||
pkColumns: [],
|
||||
readOnly: true,
|
||||
@@ -3166,7 +3170,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
statementResultIndex: 1,
|
||||
rows: [row],
|
||||
columns: ['affectedRows'],
|
||||
messages: Array.isArray(res.messages) ? res.messages : [],
|
||||
messages: legacyResultMessages,
|
||||
pkColumns: [],
|
||||
readOnly: true
|
||||
});
|
||||
@@ -3382,9 +3386,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
|
||||
// res.data 是 ResultSetData[] 数组
|
||||
const resultSetDataArray = Array.isArray(res.data) ? (res.data as any[]) : [];
|
||||
const topLevelMessages = Array.isArray(res.messages)
|
||||
? (res.messages as any[]).map((item) => String(item ?? '').trim()).filter(Boolean)
|
||||
: [];
|
||||
const topLevelMessages = normalizeQueryResultMessages(res.messages);
|
||||
const nextResultSets: ResultSet[] = [];
|
||||
const maxRows = Number(queryOptions?.maxRows) || 0;
|
||||
let anyTruncated = false;
|
||||
@@ -3398,7 +3400,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const plan = executablePlans[Math.max(0, sourceStatementIndex - 1)];
|
||||
const originalSql = plan?.originalSql || '';
|
||||
const executedSql = plan?.executedSql || originalSql;
|
||||
const resultMessages = Array.isArray(rsData?.messages) ? rsData.messages : [];
|
||||
const resultMessages = normalizeQueryResultMessages(rsData?.messages);
|
||||
|
||||
// 检查是否为 affectedRows 类结果集
|
||||
const isAffectedResult = Array.isArray(rsData.rows) && rsData.rows.length === 1
|
||||
@@ -3591,10 +3593,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const editorHasFocus = !!editor.hasTextFocus?.();
|
||||
const inEditorPane = !!(targetNode && editorPaneRef.current?.contains(targetNode));
|
||||
const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode));
|
||||
if (!editorHasFocus && !inEditorPane) {
|
||||
if (isEditableElement(event.target) && !inEditorPane) {
|
||||
return;
|
||||
}
|
||||
if (!editorHasFocus && isEditableElement(event.target) && !inEditorPane) {
|
||||
if (!editorHasFocus && !inEditorPane) {
|
||||
return;
|
||||
}
|
||||
if (!editorHasFocus && !inQueryEditor) {
|
||||
|
||||
@@ -35,6 +35,9 @@ const requiredKeys = [
|
||||
'query_editor.results_panel.tab.message',
|
||||
'query_editor.results_panel.tab.result',
|
||||
'query_editor.results_panel.message.title',
|
||||
'query_editor.results_panel.message.action.copy',
|
||||
'query_editor.results_panel.message.copy_unsupported',
|
||||
'query_editor.results_panel.message.copy_failed',
|
||||
'query_editor.results_panel.panel.title',
|
||||
'query_editor.empty_state.title',
|
||||
'query_editor.empty_state.description',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, Dropdown, Tabs, Tooltip, type MenuProps } from 'antd';
|
||||
import { BugOutlined, CloseOutlined, EyeInvisibleOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, Tabs, Tooltip, message, type MenuProps } from 'antd';
|
||||
import { BugOutlined, CloseOutlined, CopyOutlined, EyeInvisibleOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { EditRowLocator } from '../utils/rowLocator';
|
||||
import type { QueryResultPaginationState } from '../utils/queryResultPagination';
|
||||
@@ -89,6 +89,111 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
|
||||
const hideTooltipTitle = toggleShortcutLabel
|
||||
? t('query_editor.results_panel.tooltip.hide_with_shortcut', { shortcut: toggleShortcutLabel })
|
||||
: t('query_editor.results_panel.tooltip.hide');
|
||||
const handleMessageTextareaKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== 'a') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.focus();
|
||||
event.currentTarget.select();
|
||||
};
|
||||
const handleCopyMessageText = async (text: string) => {
|
||||
const safeText = String(text || '');
|
||||
if (!safeText.trim()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (typeof navigator?.clipboard?.writeText !== 'function') {
|
||||
throw new Error(t('query_editor.results_panel.message.copy_unsupported'));
|
||||
}
|
||||
await navigator.clipboard.writeText(safeText);
|
||||
message.success(t('data_grid.message.copied_to_clipboard'));
|
||||
} catch (error: any) {
|
||||
message.error(t('query_editor.results_panel.message.copy_failed', {
|
||||
detail: error?.message || t('common.unknown'),
|
||||
}));
|
||||
}
|
||||
};
|
||||
const renderMessageBlock = ({
|
||||
text,
|
||||
title,
|
||||
fontSize,
|
||||
fillHeight = false,
|
||||
compact = false,
|
||||
maxWidth,
|
||||
color,
|
||||
marginTop,
|
||||
}: {
|
||||
text: string;
|
||||
title?: string;
|
||||
fontSize: string;
|
||||
fillHeight?: boolean;
|
||||
compact?: boolean;
|
||||
maxWidth?: number;
|
||||
color: string;
|
||||
marginTop?: number;
|
||||
}) => (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: compact ? 8 : 12,
|
||||
padding: compact ? 12 : 16,
|
||||
borderRadius: 8,
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : '#fff',
|
||||
textAlign: 'left',
|
||||
marginTop,
|
||||
width: maxWidth ? `min(100%, ${maxWidth}px)` : '100%',
|
||||
flex: fillHeight ? 1 : undefined,
|
||||
minHeight: fillHeight ? 0 : undefined,
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: title ? 'space-between' : 'flex-end',
|
||||
gap: 12,
|
||||
}}>
|
||||
{title ? <span style={{ fontSize: 14, fontWeight: 600 }}>{title}</span> : <span />}
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => { void handleCopyMessageText(text); }}
|
||||
disabled={!text.trim()}
|
||||
>
|
||||
{t('query_editor.results_panel.message.action.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
<textarea
|
||||
readOnly
|
||||
wrap="soft"
|
||||
spellCheck={false}
|
||||
aria-label={title || t('query_editor.results_panel.message.title')}
|
||||
data-query-result-message-textarea={compact ? 'compact' : 'full'}
|
||||
value={text}
|
||||
onKeyDown={handleMessageTextareaKeyDown}
|
||||
style={{
|
||||
width: '100%',
|
||||
flex: fillHeight ? 1 : undefined,
|
||||
minHeight: compact ? 72 : 0,
|
||||
maxHeight: compact ? 160 : undefined,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
border: 'none',
|
||||
resize: 'none',
|
||||
background: 'transparent',
|
||||
color,
|
||||
fontFamily: 'var(--gn-font-mono)',
|
||||
fontSize,
|
||||
lineHeight: 1.6,
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const toolbarHideButton = (
|
||||
<Tooltip title={hideTooltipTitle}>
|
||||
<Button
|
||||
@@ -184,30 +289,26 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
|
||||
),
|
||||
children: (() => {
|
||||
if (rs.resultType === 'message') {
|
||||
const messageText = (rs.messages || []).join('\n');
|
||||
return (
|
||||
<div className={isV2Ui ? 'gn-v2-query-success' : undefined} style={{
|
||||
flex: 1, minHeight: 0, display: 'flex', justifyContent: 'center',
|
||||
flex: 1, minHeight: 0, display: 'flex', justifyContent: 'flex-start',
|
||||
flexDirection: 'column', gap: 12, padding: 24, color: '#666', userSelect: 'text',
|
||||
overflow: 'auto',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>{t('query_editor.results_panel.message.title')}</span>
|
||||
<div style={{
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : '#fff',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'var(--gn-font-mono)',
|
||||
{renderMessageBlock({
|
||||
text: messageText,
|
||||
title: t('query_editor.results_panel.message.title'),
|
||||
fontSize: 'var(--gn-font-size-mono, 13px)',
|
||||
}}>
|
||||
{(rs.messages || []).join('\n')}
|
||||
</div>
|
||||
fillHeight: true,
|
||||
color: darkMode ? '#d4d4d4' : '#333',
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isAffectedRowsResult(rs)) {
|
||||
const affected = Number(rs.rows[0]?.affectedRows ?? 0);
|
||||
const messageText = Array.isArray(rs.messages) ? rs.messages.join('\n') : '';
|
||||
return (
|
||||
<div className={isV2Ui ? 'gn-v2-query-success' : undefined} style={{
|
||||
flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
@@ -216,44 +317,33 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
|
||||
<span style={{ fontSize: 36, color: '#52c41a' }}>✓</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 500 }}>{t('query_editor.result.execution_success')}</span>
|
||||
<span style={{ fontSize: 13, color: '#999' }}>{t('query_editor.result.affected_rows', { count: affected })}</span>
|
||||
{Array.isArray(rs.messages) && rs.messages.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: 8,
|
||||
maxWidth: 720,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : '#fff',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'var(--gn-font-mono)',
|
||||
{messageText
|
||||
? renderMessageBlock({
|
||||
text: messageText,
|
||||
fontSize: 'var(--gn-font-size-mono, 12px)',
|
||||
}}>
|
||||
{rs.messages.join('\n')}
|
||||
</div>
|
||||
)}
|
||||
compact: true,
|
||||
maxWidth: 720,
|
||||
color: darkMode ? '#d4d4d4' : '#666',
|
||||
marginTop: 8,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
{Array.isArray(rs.messages) && rs.messages.length > 0 && (
|
||||
<div style={{
|
||||
flex: '0 0 auto',
|
||||
margin: '8px 8px 0',
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : '#fff',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'var(--gn-font-mono)',
|
||||
fontSize: 'var(--gn-font-size-mono, 12px)',
|
||||
color: darkMode ? '#d4d4d4' : '#666',
|
||||
}}>
|
||||
{rs.messages.join('\n')}
|
||||
</div>
|
||||
)}
|
||||
{Array.isArray(rs.messages) && rs.messages.length > 0
|
||||
? (
|
||||
<div style={{ flex: '0 0 auto', margin: '8px 8px 0' }}>
|
||||
{renderMessageBlock({
|
||||
text: rs.messages.join('\n'),
|
||||
fontSize: 'var(--gn-font-size-mono, 12px)',
|
||||
compact: true,
|
||||
color: darkMode ? '#d4d4d4' : '#666',
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
<DataGrid
|
||||
data={rs.rows}
|
||||
columnNames={rs.columns}
|
||||
|
||||
@@ -25,6 +25,7 @@ export type CompletionTriggerMeta = {dbName: string, triggerName: string, tableN
|
||||
export type CompletionRoutineMeta = {dbName: string, routineName: string, routineType: string, schemaName?: string};
|
||||
|
||||
export const QUERY_LOCATOR_ALIAS_PREFIX = '__gonavi_locator_';
|
||||
const SQLSERVER_MESSAGE_PREFIX_RE = /^\s*mssql:\s*/i;
|
||||
|
||||
export const buildQueryReadOnlyLocator = (reason: string): EditRowLocator => ({
|
||||
strategy: 'none',
|
||||
@@ -120,6 +121,36 @@ export const stripQueryIdentifierQuotes = (part: string): string => {
|
||||
return text;
|
||||
};
|
||||
|
||||
export const normalizeQueryResultMessageText = (message: unknown): string => {
|
||||
const text = String(message ?? '').replace(/\r\n?/g, '\n');
|
||||
if (!text.trim()) return '';
|
||||
|
||||
let prefixRemoved = false;
|
||||
const normalizedLines = text
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (!line.trim()) return '';
|
||||
if (SQLSERVER_MESSAGE_PREFIX_RE.test(line)) {
|
||||
prefixRemoved = true;
|
||||
return line.replace(SQLSERVER_MESSAGE_PREFIX_RE, '').trimStart();
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
const normalized = (prefixRemoved
|
||||
? normalizedLines.map((line) => line.trim() ? line.trimStart() : '').join('\n')
|
||||
: normalizedLines.join('\n'))
|
||||
.trim();
|
||||
|
||||
return prefixRemoved ? normalized : text.trim();
|
||||
};
|
||||
|
||||
export const normalizeQueryResultMessages = (messages: unknown): string[] => (
|
||||
Array.isArray(messages)
|
||||
? messages.map((item) => normalizeQueryResultMessageText(item)).filter(Boolean)
|
||||
: []
|
||||
);
|
||||
|
||||
export const MYSQL_SYSTEM_METADATA_SCHEMAS = new Set(['information_schema', 'performance_schema', 'mysql', 'sys']);
|
||||
export const POSTGRES_SYSTEM_METADATA_SCHEMAS = new Set(['information_schema', 'pg_catalog']);
|
||||
export const SQLITE_SYSTEM_METADATA_TABLES = new Set(['sqlite_master', 'sqlite_schema', 'sqlite_temp_master', 'sqlite_temp_schema']);
|
||||
|
||||
@@ -2263,6 +2263,9 @@
|
||||
"query_editor.results_panel.tab.message": "Meldung {{index}}",
|
||||
"query_editor.results_panel.tab.result": "Ergebnis {{index}}",
|
||||
"query_editor.results_panel.message.title": "Ausführungsmeldungen",
|
||||
"query_editor.results_panel.message.action.copy": "Kopieren",
|
||||
"query_editor.results_panel.message.copy_unsupported": "Die Zwischenablage ist in der aktuellen Umgebung nicht verfügbar",
|
||||
"query_editor.results_panel.message.copy_failed": "Nachricht konnte nicht kopiert werden: {{detail}}",
|
||||
"query_editor.results_panel.panel.title": "Ergebnisbereich",
|
||||
"query_editor.save_modal.title": "Abfrage speichern",
|
||||
"query_editor.save_modal.rename_title": "Abfrage umbenennen",
|
||||
|
||||
@@ -2273,6 +2273,9 @@
|
||||
"query_editor.results_panel.tab.message": "Message {{index}}",
|
||||
"query_editor.results_panel.tab.result": "Result {{index}}",
|
||||
"query_editor.results_panel.message.title": "Execution messages",
|
||||
"query_editor.results_panel.message.action.copy": "Copy",
|
||||
"query_editor.results_panel.message.copy_unsupported": "Clipboard is not available in the current environment",
|
||||
"query_editor.results_panel.message.copy_failed": "Failed to copy message: {{detail}}",
|
||||
"query_editor.results_panel.panel.title": "Results panel",
|
||||
"query_editor.save_modal.title": "Save query",
|
||||
"query_editor.save_modal.rename_title": "Rename query",
|
||||
|
||||
@@ -2263,6 +2263,9 @@
|
||||
"query_editor.results_panel.tab.message": "メッセージ {{index}}",
|
||||
"query_editor.results_panel.tab.result": "結果 {{index}}",
|
||||
"query_editor.results_panel.message.title": "実行メッセージ",
|
||||
"query_editor.results_panel.message.action.copy": "コピー",
|
||||
"query_editor.results_panel.message.copy_unsupported": "現在の環境ではクリップボードへコピーできません",
|
||||
"query_editor.results_panel.message.copy_failed": "メッセージのコピーに失敗しました: {{detail}}",
|
||||
"query_editor.results_panel.panel.title": "結果エリア",
|
||||
"query_editor.save_modal.title": "クエリを保存",
|
||||
"query_editor.save_modal.rename_title": "クエリ名を変更",
|
||||
|
||||
@@ -2263,6 +2263,9 @@
|
||||
"query_editor.results_panel.tab.message": "Сообщение {{index}}",
|
||||
"query_editor.results_panel.tab.result": "Результат {{index}}",
|
||||
"query_editor.results_panel.message.title": "Сообщения выполнения",
|
||||
"query_editor.results_panel.message.action.copy": "Копировать",
|
||||
"query_editor.results_panel.message.copy_unsupported": "Буфер обмена недоступен в текущей среде",
|
||||
"query_editor.results_panel.message.copy_failed": "Не удалось скопировать сообщение: {{detail}}",
|
||||
"query_editor.results_panel.panel.title": "Область результатов",
|
||||
"query_editor.save_modal.title": "Сохранить запрос",
|
||||
"query_editor.save_modal.rename_title": "Переименовать запрос",
|
||||
|
||||
@@ -2273,6 +2273,9 @@
|
||||
"query_editor.results_panel.tab.message": "消息 {{index}}",
|
||||
"query_editor.results_panel.tab.result": "结果 {{index}}",
|
||||
"query_editor.results_panel.message.title": "执行消息",
|
||||
"query_editor.results_panel.message.action.copy": "复制",
|
||||
"query_editor.results_panel.message.copy_unsupported": "当前环境不支持复制到剪贴板",
|
||||
"query_editor.results_panel.message.copy_failed": "复制消息失败:{{detail}}",
|
||||
"query_editor.results_panel.panel.title": "结果区",
|
||||
"query_editor.save_modal.title": "保存查询",
|
||||
"query_editor.save_modal.rename_title": "重命名查询",
|
||||
|
||||
@@ -2263,6 +2263,9 @@
|
||||
"query_editor.results_panel.tab.message": "訊息 {{index}}",
|
||||
"query_editor.results_panel.tab.result": "結果 {{index}}",
|
||||
"query_editor.results_panel.message.title": "執行訊息",
|
||||
"query_editor.results_panel.message.action.copy": "複製",
|
||||
"query_editor.results_panel.message.copy_unsupported": "目前環境不支援複製到剪貼簿",
|
||||
"query_editor.results_panel.message.copy_failed": "複製訊息失敗:{{detail}}",
|
||||
"query_editor.results_panel.panel.title": "結果區",
|
||||
"query_editor.save_modal.title": "儲存查詢",
|
||||
"query_editor.save_modal.rename_title": "重新命名查詢",
|
||||
|
||||
Reference in New Issue
Block a user