🐛 fix(query-editor): 修复消息结果前缀与复制全选交互

- 统一清洗 SQL Server 消息前缀并覆盖结果刷新与分页回填链路
- 将消息结果区改为只读文本区,补充一键复制入口
- 放行编辑器外可编辑区域的 cmd/ctrl+a,避免消息内容全选被抢占
- 补充结果面板交互与国际化回归,确保构建通过
This commit is contained in:
Syngnat
2026-06-23 14:43:13 +08:00
parent 8da8cc7f91
commit 3205d131c9
11 changed files with 276 additions and 61 deletions

View File

@@ -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');

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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}

View File

@@ -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']);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "クエリ名を変更",

View File

@@ -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": "Переименовать запрос",

View File

@@ -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": "重命名查询",

View File

@@ -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": "重新命名查詢",