feat(editor): add a SQL word-wrap toolbar toggle
This commit is contained in:
2 files changed
+121
-1
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onUnmounted, ref, watch, watchEffect } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Play, CirclePlay, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2, SpellCheck2, Layers, MoreHorizontal, BetweenVerticalStart, Eye } from "@lucide/vue";
|
||||
import { Play, CirclePlay, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2, SpellCheck2, Layers, MoreHorizontal, BetweenVerticalStart, Eye, WrapText } from "@lucide/vue";
|
||||
import { supportsInsertValueHints } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
@@ -254,6 +254,11 @@ const explainAnalyzeTooltip = computed(() => {
|
||||
const canSaveSql = computed(() => canSaveSqlTab(props.activeTab));
|
||||
const keywordCaseIsLower = computed(() => props.sqlKeywordCase === "lower");
|
||||
const keywordCaseToggleTooltip = computed(() => (keywordCaseIsLower.value ? t("toolbar.keywordCaseUpper") : t("toolbar.keywordCaseLower")));
|
||||
const wordWrapEnabled = computed(() => props.activeTab.forceWordWrap === true || settingsStore.editorSettings.wordWrap);
|
||||
function toggleWordWrap() {
|
||||
if (props.activeTab.forceWordWrap) return;
|
||||
settingsStore.updateEditorSettings({ wordWrap: !wordWrapEnabled.value });
|
||||
}
|
||||
const sqlSemanticDiagnosticsEnabled = computed(() => settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled);
|
||||
const sqlSemanticDiagnosticsToggleTooltip = computed(() => (sqlSemanticDiagnosticsEnabled.value ? t("toolbar.sqlSemanticDiagnosticsToggleOn") : t("toolbar.sqlSemanticDiagnosticsToggleOff")));
|
||||
const supportsSqlSemanticDiagnosticsToggle = computed(() => {
|
||||
@@ -359,6 +364,7 @@ const showFormatButton = computed(() => canFormatSql.value && toolbarTier.value
|
||||
const showExplainAnalyzeToggle = computed(() => toolbarTier.value < 3);
|
||||
const showCompressButton = computed(() => toolbarTier.value < 1);
|
||||
const showKeywordCaseButton = computed(() => toolbarTier.value < 1);
|
||||
const showWordWrapButton = computed(() => toolbarTier.value < 1);
|
||||
const showSemanticDiagnosticsButton = computed(() => supportsSqlSemanticDiagnosticsToggle.value && toolbarTier.value < 1);
|
||||
const showPreviewButton = computed(() => previewButtonVisible.value && toolbarTier.value < 1);
|
||||
const showInsertValueHintsButton = computed(() => supportsInsertValueHintsToggle.value && toolbarTier.value < 1);
|
||||
@@ -509,6 +515,23 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ keywordCaseToggleTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="showWordWrapButton">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
:class="wordWrapEnabled ? 'bg-sky-500/10 text-sky-700 hover:bg-sky-500/20 hover:text-sky-800 dark:text-sky-300 dark:hover:text-sky-200' : 'text-muted-foreground/50 hover:bg-muted hover:text-muted-foreground'"
|
||||
:disabled="activeTab.forceWordWrap === true"
|
||||
:aria-label="t('settings.wordWrap')"
|
||||
:aria-pressed="wordWrapEnabled"
|
||||
@click="toggleWordWrap"
|
||||
>
|
||||
<WrapText class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("settings.wordWrap") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="showSemanticDiagnosticsButton">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
@@ -613,6 +636,10 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</span>
|
||||
{{ keywordCaseToggleTooltip }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuCheckboxItem :model-value="wordWrapEnabled" :disabled="activeTab.forceWordWrap === true" @select.prevent="toggleWordWrap">
|
||||
<WrapText class="h-3.5 w-3.5" />
|
||||
{{ t("settings.wordWrap") }}
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem v-if="supportsSqlSemanticDiagnosticsToggle" :model-value="sqlSemanticDiagnosticsEnabled" @select.prevent="toggleSqlSemanticDiagnostics">
|
||||
<SpellCheck2 class="h-3.5 w-3.5" />
|
||||
{{ t("settings.sqlSemanticDiagnosticsEnabled") }}
|
||||
|
||||
@@ -63,6 +63,7 @@ vi.mock("@/components/common/ProductionContextBadge.vue", () => ({
|
||||
|
||||
import EditorToolbar from "../EditorToolbar.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { resolveExecutableSql, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
@@ -307,6 +308,98 @@ describe("EditorToolbar mount contract", () => {
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("toggles word wrap from the SQL toolbar and updates the mounted editor", async () => {
|
||||
const connectionStore = useConnectionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const connection = {
|
||||
id: "conn-1",
|
||||
name: "conn",
|
||||
db_type: "mysql",
|
||||
color: "",
|
||||
} as never;
|
||||
connectionStore.connections = [connection];
|
||||
settingsStore.editorSettings.wordWrap = false;
|
||||
|
||||
const sql = "SELECT this_is_a_very_long_column_name FROM this_is_a_very_long_table_name";
|
||||
const forceWordWrap = ref(false);
|
||||
const host = createHost();
|
||||
const app = createApp({
|
||||
setup() {
|
||||
return () =>
|
||||
h("div", [
|
||||
h(QueryEditor, {
|
||||
modelValue: sql,
|
||||
tabId: "tab-1",
|
||||
connectionId: "conn-1",
|
||||
database: "db",
|
||||
databaseType: "mysql",
|
||||
dialect: "mysql",
|
||||
forceWordWrap: forceWordWrap.value,
|
||||
autoFocus: false,
|
||||
}),
|
||||
h(EditorToolbar, {
|
||||
activeTab: {
|
||||
id: "tab-1",
|
||||
title: "SQL",
|
||||
connectionId: "conn-1",
|
||||
database: "db",
|
||||
sql,
|
||||
mode: "query",
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
forceWordWrap: forceWordWrap.value,
|
||||
},
|
||||
activeConnection: connection,
|
||||
executableSql: sql,
|
||||
explainMode: "explain",
|
||||
blockDangerousRedisCommands: false,
|
||||
sqlKeywordCase: "preserve",
|
||||
databaseRequiredSignal: 0,
|
||||
autoCommit: true,
|
||||
txnSessionId: undefined,
|
||||
txnAutoRolledBack: false,
|
||||
txnPossiblyDirty: false,
|
||||
stickyProvenReadOnlyState: false,
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.mount(host);
|
||||
await vi.waitFor(() => expect(host.querySelector(".cm-editor")).not.toBeNull(), { timeout: 5000 });
|
||||
|
||||
const view = EditorView.findFromDOM(host.querySelector(".cm-editor") as HTMLElement)!;
|
||||
expect(view.contentDOM.classList.contains("cm-lineWrapping")).toBe(false);
|
||||
|
||||
const wordWrapButton = host.querySelector<HTMLButtonElement>('button[aria-label="settings.wordWrap"]');
|
||||
expect(wordWrapButton).not.toBeNull();
|
||||
expect(wordWrapButton?.getAttribute("aria-pressed")).toBe("false");
|
||||
wordWrapButton?.click();
|
||||
|
||||
await vi.waitFor(() => expect(settingsStore.editorSettings.wordWrap).toBe(true));
|
||||
await nextTick();
|
||||
await vi.waitFor(() => expect(view.contentDOM.classList.contains("cm-lineWrapping")).toBe(true));
|
||||
expect(wordWrapButton?.getAttribute("aria-pressed")).toBe("true");
|
||||
|
||||
wordWrapButton?.click();
|
||||
await vi.waitFor(() => expect(settingsStore.editorSettings.wordWrap).toBe(false));
|
||||
await vi.waitFor(() => expect(view.contentDOM.classList.contains("cm-lineWrapping")).toBe(false));
|
||||
expect(wordWrapButton?.getAttribute("aria-pressed")).toBe("false");
|
||||
|
||||
forceWordWrap.value = true;
|
||||
await nextTick();
|
||||
await vi.waitFor(() => expect(view.contentDOM.classList.contains("cm-lineWrapping")).toBe(true));
|
||||
expect(wordWrapButton?.disabled).toBe(true);
|
||||
expect(wordWrapButton?.getAttribute("aria-pressed")).toBe("true");
|
||||
wordWrapButton?.click();
|
||||
expect(settingsStore.editorSettings.wordWrap).toBe(false);
|
||||
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("does not expose SQL formatting for Redis connections", async () => {
|
||||
const connectionStore = useConnectionStore();
|
||||
connectionStore.connections = [
|
||||
|
||||
Reference in new issue
Block a user