fix(editor): copy full error text from multi-statement execution summary
This commit is contained in:
parent
c4289e6f21
commit
0107d5fafb
|
|
@ -7,7 +7,7 @@ import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
|
|||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Check, CheckSquare2, Columns3Cog, EyeOff, Loader2, Search, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus, ShieldAlert, AlignLeft, AlignRight, PanelsTopLeft } from "@lucide/vue";
|
||||
import { Check, CheckSquare2, Columns3Cog, Copy, EyeOff, Loader2, Search, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus, ShieldAlert, AlignLeft, AlignRight, PanelsTopLeft } from "@lucide/vue";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -96,6 +96,7 @@ import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
|
|||
import { isDataGridToolbarCompact, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar";
|
||||
import { useTabScroll } from "@/composables/useTabScroll";
|
||||
import { formatElapsedSeconds } from "@/lib/common/elapsedTime";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import type { CustomSaveHandler } from "@/composables/useDataGridEditor";
|
||||
import type { QueryTab, ConnectionConfig, TableInfoTab, TreeNode, VectorCollectionMeta, ObjectBrowserViewport } from "@/types/database";
|
||||
import type { SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation";
|
||||
|
|
@ -900,6 +901,15 @@ function focusExecutionSummaryItem(item: ExecutionSummaryItem) {
|
|||
queryEditorRef.value?.focusStatementRange(executionSummaryItemRange(item) ?? null);
|
||||
}
|
||||
|
||||
async function copyExecutionSummaryError(error: string) {
|
||||
try {
|
||||
await copyToClipboard(error);
|
||||
toast(t("grid.copied"));
|
||||
} catch (copyError: any) {
|
||||
toast(t("grid.copyFailed", { message: copyError?.message || String(copyError) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleModRTarget(target: Element): boolean {
|
||||
if (target.closest("[data-query-editor-root]")) return queryEditorRef.value?.openReplace() ?? false;
|
||||
if (target.closest("[data-cell-detail-editor-root]")) return dataGridRef.value?.openCellDetailSearch() ?? false;
|
||||
|
|
@ -1406,22 +1416,29 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
|||
<div class="text-right">{{ t("executionSummary.affected") }}</div>
|
||||
<div class="text-right">{{ t("executionSummary.time") }}</div>
|
||||
</div>
|
||||
<button
|
||||
v-for="item in summaryItems"
|
||||
:key="item.statementIndex"
|
||||
type="button"
|
||||
class="grid w-full grid-cols-[4rem_minmax(14rem,1fr)_7rem_7rem_6rem] items-center border-b px-3 py-2 text-left text-xs transition-colors last:border-b-0 hover:bg-muted/35 focus-visible:bg-muted/40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-primary"
|
||||
:title="item.error || item.sql"
|
||||
@click="previewExecutionSummaryItem(item)"
|
||||
@dblclick="focusExecutionSummaryItem(item)"
|
||||
@keydown.enter.prevent="focusExecutionSummaryItem(item)"
|
||||
>
|
||||
<div class="font-mono text-muted-foreground">#{{ item.statementIndex + 1 }}</div>
|
||||
<div class="min-w-0">
|
||||
<div v-for="item in summaryItems" :key="item.statementIndex" class="relative grid w-full grid-cols-[4rem_minmax(14rem,1fr)_7rem_7rem_6rem] items-center border-b px-3 py-2 text-left text-xs last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-0 z-0 cursor-pointer text-left transition-colors hover:bg-muted/35 focus-visible:bg-muted/40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-primary"
|
||||
:title="item.error || item.sql"
|
||||
:aria-label="item.error || item.sql || t('executionSummary.noSql')"
|
||||
@click="previewExecutionSummaryItem(item)"
|
||||
@dblclick="focusExecutionSummaryItem(item)"
|
||||
@keydown.enter.prevent="focusExecutionSummaryItem(item)"
|
||||
/>
|
||||
<div class="pointer-events-none relative z-[1] font-mono text-muted-foreground">#{{ item.statementIndex + 1 }}</div>
|
||||
<div class="relative z-[1] min-w-0 cursor-pointer" @click="previewExecutionSummaryItem(item)" @dblclick="focusExecutionSummaryItem(item)">
|
||||
<div class="truncate font-mono text-[11px] text-foreground">{{ item.sql || t("executionSummary.noSql") }}</div>
|
||||
<div v-if="item.error" class="mt-0.5 truncate text-[11px] text-destructive">{{ item.error }}</div>
|
||||
<div v-if="item.error" class="mt-0.5 flex min-w-0 items-center gap-1 text-[11px] text-destructive">
|
||||
<span data-native-clipboard class="min-w-0 flex-1 cursor-text select-text truncate" :title="item.error" @mousedown.stop @click.stop @dblclick.stop>{{ item.error }}</span>
|
||||
<LightTooltip :text="t('grid.copy')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
||||
<button type="button" class="pointer-events-auto flex h-5 w-5 shrink-0 items-center justify-center rounded text-destructive/70 hover:bg-destructive/10 hover:text-destructive" :aria-label="t('grid.copy')" @mousedown.stop @click.stop="copyExecutionSummaryError(item.error)">
|
||||
<Copy class="h-3 w-3" />
|
||||
</button>
|
||||
</LightTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="pointer-events-none relative z-[1]">
|
||||
<span
|
||||
class="inline-flex h-5 items-center gap-1 rounded-full border px-2 text-[10px]"
|
||||
:class="{
|
||||
|
|
@ -1439,9 +1456,9 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
|||
{{ t(`executionSummary.statuses.${item.status}`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-right tabular-nums">{{ item.status === "pending" || item.status === "running" || item.status === "skipped" ? "—" : item.affectedRows.toLocaleString() }}</div>
|
||||
<div class="text-right tabular-nums">{{ item.executionTimeMs > 0 || item.status === "success" || item.status === "error" ? `${item.executionTimeMs}ms` : "—" }}</div>
|
||||
</button>
|
||||
<div class="pointer-events-none relative z-[1] text-right tabular-nums">{{ item.status === "pending" || item.status === "running" || item.status === "skipped" ? "—" : item.affectedRows.toLocaleString() }}</div>
|
||||
<div class="pointer-events-none relative z-[1] text-right tabular-nums">{{ item.executionTimeMs > 0 || item.status === "success" || item.status === "error" ? `${item.executionTimeMs}ms` : "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-3 py-2 text-[11px] text-muted-foreground">{{ t("executionSummary.navigationHint") }}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,3 +54,13 @@ describe("QueryEditor execution routing", () => {
|
|||
expect(queryEditorSource).toContain("if (options.bypassPicker || !settingsStore.editorSettings.showExecutionTargetPicker");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContentArea execution summary errors", () => {
|
||||
it("keeps batch errors selectable and copyable without triggering statement navigation", () => {
|
||||
expect(contentAreaSource).toContain('class="absolute inset-0 z-0 cursor-pointer');
|
||||
expect(contentAreaSource).toContain('data-native-clipboard class="min-w-0 flex-1 cursor-text select-text truncate"');
|
||||
expect(contentAreaSource).toContain("@mousedown.stop @click.stop @dblclick.stop");
|
||||
expect(contentAreaSource).toContain('@click.stop="copyExecutionSummaryError(item.error)"');
|
||||
expect(contentAreaSource).toContain("await copyToClipboard(error)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue