feat(ddl): formatted DDL display with dialog or tab mode
This commit is contained in:
1 parent
86bdf9f78a
commit
76e41a6a4d
30 files changed
+956
-51
No files matched your search
@@ -3,7 +3,7 @@ import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
|
||||
import { computed, nextTick, onUnmounted, ref, shallowRef, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Clipboard, Loader2, RefreshCw } from "@lucide/vue";
|
||||
import { Clipboard, ExternalLink, Loader2, RefreshCw } from "@lucide/vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
@@ -11,11 +11,13 @@ import { loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { formatSqlForDisplay, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { applyDdlDatabaseQualifier, ddlFormatDialectFor, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
|
||||
import type { SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { ddlFormatDialectFor, formatDdlForDisplay } from "@/lib/sql/ddlDisplay";
|
||||
import { loadObjectDdl } from "@/lib/metadata/objectDdlCache";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { HelpTooltip } from "@/components/ui/tooltip";
|
||||
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
@@ -50,13 +52,16 @@ const { toast } = useToast();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const rawDdlContent = ref("");
|
||||
const ddlContent = computed(() => applyDdlStoragePreference(rawDdlContent.value, props.databaseType, settingsStore.editorSettings.excludeDdlStorage));
|
||||
const originalDdlContent = ref("");
|
||||
const formattedDdlContent = ref("");
|
||||
const ddlDisplayMode = ref<"formatted" | "original">("formatted");
|
||||
const ddlContent = computed(() => applyDdlStoragePreference(ddlDisplayMode.value === "formatted" ? formattedDdlContent.value : originalDdlContent.value, props.databaseType, settingsStore.editorSettings.excludeDdlStorage));
|
||||
const ddlLoading = ref(false);
|
||||
const ddlError = ref("");
|
||||
const ddlEditorContainer = ref<HTMLDivElement>();
|
||||
const ddlSearchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
|
||||
const ddlEditorView = shallowRef<EditorView | null>(null);
|
||||
let ddlEditorResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// Keep the dialog movable for the duration of one open cycle. This mirrors the
|
||||
// existing draggable dialogs without persisting a potentially off-screen position.
|
||||
@@ -65,14 +70,25 @@ const isDragging = ref(false);
|
||||
const dragStartPosition = ref({ x: 0, y: 0 });
|
||||
const dragStartOffset = ref({ x: 0, y: 0 });
|
||||
const activePointerId = ref<number | null>(null);
|
||||
const dialogSize = ref({ width: 980, height: 720 });
|
||||
const isResizing = ref(false);
|
||||
const resizeStartPosition = ref({ x: 0, y: 0 });
|
||||
const resizeStartSize = ref({ width: 980, height: 720 });
|
||||
const resizeStartOffset = ref({ x: 0, y: 0 });
|
||||
const resizeStartRect = ref({ left: 0, top: 0 });
|
||||
const activeResizePointerId = ref<number | null>(null);
|
||||
const dialogContentStyle = computed(() => {
|
||||
if (isDragging.value || dragOffset.value.x !== 0 || dragOffset.value.y !== 0) {
|
||||
return {
|
||||
transform: `translate(${dragOffset.value.x}px, ${dragOffset.value.y}px)`,
|
||||
transition: isDragging.value ? "none" : "transform 0.15s ease-out",
|
||||
};
|
||||
}
|
||||
return {};
|
||||
const moved = isDragging.value || dragOffset.value.x !== 0 || dragOffset.value.y !== 0;
|
||||
return {
|
||||
width: `min(${dialogSize.value.width}px, calc(100vw - 32px))`,
|
||||
height: `min(${dialogSize.value.height}px, calc(100vh - 32px))`,
|
||||
maxWidth: "calc(100vw - 32px)",
|
||||
maxHeight: "calc(100vh - 32px)",
|
||||
minWidth: "min(560px, calc(100vw - 32px))",
|
||||
minHeight: "min(420px, calc(100vh - 32px))",
|
||||
transform: moved ? `translate(${dragOffset.value.x}px, ${dragOffset.value.y}px)` : undefined,
|
||||
transition: isDragging.value || isResizing.value ? "none" : "transform 0.15s ease-out",
|
||||
};
|
||||
});
|
||||
|
||||
// The CodeMirror editor (if any) that was focused when this dialog opened, so focus can be
|
||||
@@ -83,6 +99,9 @@ function resetDialogDragOffset() {
|
||||
dragOffset.value = { x: 0, y: 0 };
|
||||
isDragging.value = false;
|
||||
activePointerId.value = null;
|
||||
dialogSize.value = { width: 980, height: 720 };
|
||||
isResizing.value = false;
|
||||
activeResizePointerId.value = null;
|
||||
}
|
||||
|
||||
function startDialogDrag(event: PointerEvent) {
|
||||
@@ -113,6 +132,46 @@ function endDialogDrag(event: PointerEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function startDialogResize(event: PointerEvent) {
|
||||
if (event.button !== undefined && event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
isResizing.value = true;
|
||||
activeResizePointerId.value = event.pointerId;
|
||||
resizeStartPosition.value = { x: event.clientX, y: event.clientY };
|
||||
const dialogRect = (event.currentTarget as HTMLElement).closest<HTMLElement>("[data-slot='dialog-content']")?.getBoundingClientRect();
|
||||
resizeStartSize.value = dialogRect ? { width: dialogRect.width, height: dialogRect.height } : { ...dialogSize.value };
|
||||
resizeStartRect.value = dialogRect ? { left: dialogRect.left, top: dialogRect.top } : { left: 0, top: 0 };
|
||||
resizeStartOffset.value = { ...dragOffset.value };
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function moveDialogResize(event: PointerEvent) {
|
||||
if (!isResizing.value || event.pointerId !== activeResizePointerId.value) return;
|
||||
const maxWidth = Math.max(1, Math.min(window.innerWidth - 32, window.innerWidth - 16 - resizeStartRect.value.left));
|
||||
const maxHeight = Math.max(1, Math.min(window.innerHeight - 32, window.innerHeight - 16 - resizeStartRect.value.top));
|
||||
const minWidth = Math.min(560, maxWidth);
|
||||
const minHeight = Math.min(420, maxHeight);
|
||||
const width = Math.min(maxWidth, Math.max(minWidth, resizeStartSize.value.width + event.clientX - resizeStartPosition.value.x));
|
||||
const height = Math.min(maxHeight, Math.max(minHeight, resizeStartSize.value.height + event.clientY - resizeStartPosition.value.y));
|
||||
dialogSize.value = { width, height };
|
||||
dragOffset.value = {
|
||||
x: resizeStartOffset.value.x + (width - resizeStartSize.value.width) / 2,
|
||||
y: resizeStartOffset.value.y + (height - resizeStartSize.value.height) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function endDialogResize(event: PointerEvent) {
|
||||
if (!isResizing.value || event.pointerId !== activeResizePointerId.value) return;
|
||||
isResizing.value = false;
|
||||
activeResizePointerId.value = null;
|
||||
try {
|
||||
(event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);
|
||||
} catch {
|
||||
// Pointer capture may already have been released by the browser.
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDdl(force = false) {
|
||||
ddlError.value = "";
|
||||
ddlLoading.value = true;
|
||||
@@ -131,9 +190,19 @@ async function loadDdl(force = false) {
|
||||
{ force },
|
||||
);
|
||||
const formatDialect = ddlFormatDialectFor({ formatDialect: props.formatDialect, databaseType: props.databaseType, highlightDialect: props.dialect });
|
||||
const formatted = await formatSqlForDisplay(ddl, formatDialect, settingsStore.editorSettings.sqlFormatter);
|
||||
const unqualified = applyDdlDatabaseQualifier(formatted, formatDialect, props.databaseType, settingsStore.editorSettings.generateSqlIncludeDatabaseName, props.database, props.catalog);
|
||||
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? unqualified : omitDdlIdentifierQuotes(unqualified, formatDialect);
|
||||
originalDdlContent.value = ddl;
|
||||
formattedDdlContent.value = await formatDdlForDisplay(
|
||||
ddl,
|
||||
{
|
||||
dialect: formatDialect,
|
||||
databaseType: props.databaseType,
|
||||
database: props.database,
|
||||
catalog: props.catalog,
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
||||
},
|
||||
settingsStore.editorSettings.sqlFormatter,
|
||||
);
|
||||
} catch (e: any) {
|
||||
ddlError.value = e?.message || String(e);
|
||||
} finally {
|
||||
@@ -149,7 +218,13 @@ watch(
|
||||
if (!open) return;
|
||||
const active = document.activeElement;
|
||||
editorRootToRestoreFocus = active instanceof HTMLElement ? active.closest(".cm-editor") : null;
|
||||
rawDdlContent.value = "";
|
||||
ddlDisplayMode.value = "formatted";
|
||||
originalDdlContent.value = "";
|
||||
formattedDdlContent.value = "";
|
||||
if (settingsStore.editorSettings.ddlOpenMode === "tab") {
|
||||
openDdlViewerTabPending();
|
||||
return;
|
||||
}
|
||||
await loadDdl(settingsStore.editorSettings.refreshDdlOnOpen);
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -236,11 +311,17 @@ async function initDdlEditor(content: string) {
|
||||
});
|
||||
const editorView = new EditorView({ state, parent: ddlEditorContainer.value });
|
||||
ddlEditorView.value = editorView;
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
ddlEditorResizeObserver = new ResizeObserver(() => editorView.requestMeasure());
|
||||
ddlEditorResizeObserver.observe(ddlEditorContainer.value);
|
||||
}
|
||||
editorView.focus();
|
||||
}
|
||||
|
||||
/** Tears down the CodeMirror instance when the dialog closes. */
|
||||
function destroyDdlEditor() {
|
||||
ddlEditorResizeObserver?.disconnect();
|
||||
ddlEditorResizeObserver = null;
|
||||
ddlEditorView.value?.destroy();
|
||||
ddlEditorView.value = null;
|
||||
}
|
||||
@@ -253,15 +334,54 @@ function copyDdlContent() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the read-only DDL tab. With `ddl` empty the tab is created pending
|
||||
* (`ddlLoad`) and loads through the tab surface itself — the same "tab first,
|
||||
* load with visible state" flow as object-source tabs — so in tab mode the
|
||||
* click shows the tab and its loading state immediately instead of silently
|
||||
* waiting behind a closed dialog; a failure is shown in place with a retry.
|
||||
*/
|
||||
function createDdlViewerTab(ddl: string, pending: boolean) {
|
||||
const queryStore = useQueryStore();
|
||||
const tabId = queryStore.createTab(props.connectionId, props.database, `${t("contextMenu.viewDdl")} - ${props.tableName}`, "query", props.schema, ddl, props.catalog, { forceNew: true, sourceView: true });
|
||||
const tab = queryStore.tabs.find((item) => item.id === tabId);
|
||||
if (tab) {
|
||||
tab.ddlViewer = {
|
||||
schema: props.schema || props.database,
|
||||
tableName: props.tableName,
|
||||
objectType: props.objectType,
|
||||
formatDialect: ddlFormatDialectFor({ formatDialect: props.formatDialect, databaseType: props.databaseType, highlightDialect: props.dialect }),
|
||||
};
|
||||
if (pending) {
|
||||
tab.ddlLoad = { startedAt: Date.now() };
|
||||
void queryStore.loadDdlViewerTab(tabId);
|
||||
}
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
function openDdlInNewTab() {
|
||||
const ddl = ddlContent.value.trim();
|
||||
if (!ddl) return;
|
||||
|
||||
createDdlViewerTab(ddl, false);
|
||||
}
|
||||
|
||||
function openDdlViewerTabPending() {
|
||||
createDdlViewerTab("", true);
|
||||
}
|
||||
|
||||
watch(ddlContent, (content) => {
|
||||
const view = ddlEditorView.value;
|
||||
if (view) view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: content } });
|
||||
});
|
||||
|
||||
// When DDL finishes loading, create the editor inside the dialog.
|
||||
watch(ddlLoading, (loading) => {
|
||||
if (!loading && ddlContent.value && props.open) {
|
||||
nextTick(() => initDdlEditor(ddlContent.value));
|
||||
watch([ddlLoading, ddlContent], ([loading, content]) => {
|
||||
if (!loading && content && props.open) {
|
||||
nextTick(() => {
|
||||
if (!ddlLoading.value && props.open && ddlContent.value === content) void initDdlEditor(content);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -281,7 +401,9 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
function retry() {
|
||||
rawDdlContent.value = "";
|
||||
originalDdlContent.value = "";
|
||||
formattedDdlContent.value = "";
|
||||
ddlDisplayMode.value = "formatted";
|
||||
void loadDdl(true);
|
||||
}
|
||||
|
||||
@@ -290,6 +412,10 @@ function setRefreshDdlOnOpen(value: boolean) {
|
||||
if (value) void loadDdl(true);
|
||||
}
|
||||
|
||||
function setDdlOpenMode(value: unknown) {
|
||||
if (value === "dialog" || value === "tab") settingsStore.updateEditorSettings({ ddlOpenMode: value });
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
emit("update:open", false);
|
||||
}
|
||||
@@ -297,29 +423,40 @@ function onClose() {
|
||||
|
||||
<template>
|
||||
<Dialog :open="props.open" @update:open="onClose">
|
||||
<DialogContent :style="dialogContentStyle" class="dbx-ddl-view-dialog sm:max-w-190" @close-auto-focus="onDdlDialogCloseAutoFocus">
|
||||
<DialogHeader class="cursor-move select-none" @pointerdown="startDialogDrag" @pointermove="moveDialogDrag" @pointerup="endDialogDrag" @pointercancel="endDialogDrag">
|
||||
<DialogContent :style="dialogContentStyle" class="dbx-ddl-view-dialog flex min-h-0 flex-col overflow-hidden sm:max-w-190" @close-auto-focus="onDdlDialogCloseAutoFocus">
|
||||
<DialogHeader class="shrink-0 cursor-move select-none" @pointerdown="startDialogDrag" @pointermove="moveDialogDrag" @pointerup="endDialogDrag" @pointercancel="endDialogDrag">
|
||||
<DialogTitle>DDL - {{ props.tableName }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<div v-if="ddlLoading" class="flex min-h-80 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-3">
|
||||
<div v-if="!ddlLoading && !ddlError && ddlContent" class="flex shrink-0 items-center justify-between gap-3">
|
||||
<span class="text-sm text-muted-foreground">{{ t("contextMenu.ddlDisplayMode") }}</span>
|
||||
<div class="flex items-center gap-1" role="group" :aria-label="t('contextMenu.ddlDisplayMode')">
|
||||
<Button variant="outline" size="sm" :class="ddlDisplayMode === 'formatted' ? 'border-primary bg-accent' : ''" :aria-pressed="ddlDisplayMode === 'formatted'" @click="ddlDisplayMode = 'formatted'">
|
||||
{{ t("contextMenu.ddlDisplayFormatted") }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" :class="ddlDisplayMode === 'original' ? 'border-primary bg-accent' : ''" :aria-pressed="ddlDisplayMode === 'original'" @click="ddlDisplayMode = 'original'">
|
||||
{{ t("contextMenu.ddlDisplayOriginal") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="ddlLoading" class="flex min-h-80 flex-1 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
<span>{{ t("contextMenu.viewDdlLoading") }}</span>
|
||||
</div>
|
||||
<div v-else-if="ddlError" class="flex min-h-80 flex-col items-center justify-center gap-3 text-sm">
|
||||
<div v-else-if="ddlError" class="flex min-h-80 flex-1 flex-col items-center justify-center gap-3 text-sm">
|
||||
<p class="text-destructive">{{ ddlError }}</p>
|
||||
<Button variant="outline" size="sm" @click="retry">
|
||||
<RefreshCw />
|
||||
{{ t("common.retry") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="ddl-view-editor relative min-h-80 max-h-[60vh] overflow-hidden rounded border">
|
||||
<div v-else class="ddl-view-editor relative min-h-0 flex-1 overflow-hidden rounded border">
|
||||
<div ref="ddlEditorContainer" class="h-full" />
|
||||
<EditorSearchPanel v-if="ddlEditorView" ref="ddlSearchPanelRef" :view="ddlEditorView" />
|
||||
</div>
|
||||
</div>
|
||||
<DdlStorageToggle :database-type="props.databaseType" :disabled="ddlLoading || !!ddlError" />
|
||||
<DialogFooter>
|
||||
<DialogFooter class="shrink-0 pr-7">
|
||||
<div class="mr-auto flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Switch id="ddl-refresh-on-open" size="sm" :model-value="settingsStore.editorSettings.refreshDdlOnOpen" @update:model-value="setRefreshDdlOnOpen" />
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -328,8 +465,22 @@ function onClose() {
|
||||
{{ t("contextMenu.refreshDdlOnOpenHint") }}
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<span class="ml-3">{{ t("contextMenu.ddlOpenMode") }}</span>
|
||||
<Select :model-value="settingsStore.editorSettings.ddlOpenMode" @update:model-value="setDdlOpenMode">
|
||||
<SelectTrigger class="h-8 w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dialog">{{ t("contextMenu.ddlOpenModeDialog") }}</SelectItem>
|
||||
<SelectItem value="tab">{{ t("contextMenu.ddlOpenModeTab") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="outline" @click="onClose">{{ t("common.close") }}</Button>
|
||||
<Button variant="outline" :disabled="ddlLoading || !ddlContent" :title="t('contextMenu.openDdlInNewTab')" @click="openDdlInNewTab">
|
||||
<ExternalLink class="h-4 w-4" />
|
||||
{{ t("contextMenu.openDdlInNewTab") }}
|
||||
</Button>
|
||||
<Button variant="outline" :disabled="ddlLoading" :title="t('structureEditor.refresh')" @click="loadDdl(true)">
|
||||
<RefreshCw class="h-4 w-4" />
|
||||
{{ t("structureEditor.refresh") }}
|
||||
@@ -339,6 +490,17 @@ function onClose() {
|
||||
{{ t("grid.copyDdl") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute bottom-1 right-1 z-20 grid size-6 touch-none cursor-nwse-resize place-items-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:aria-label="t('contextMenu.resizeDdlDialog')"
|
||||
@pointerdown="startDialogResize"
|
||||
@pointermove="moveDialogResize"
|
||||
@pointerup="endDialogResize"
|
||||
@pointercancel="endDialogResize"
|
||||
>
|
||||
<span aria-hidden="true" class="h-3 w-3 border-b-2 border-r-2 border-current" />
|
||||
</button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
import { createApp, defineComponent, h, nextTick, type App } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import DdlScopeFixture from "./DdlDialog.legacyScope.fixture.vue";
|
||||
|
||||
const ddlViewDialogSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/objects/DdlViewDialog.vue"), "utf8");
|
||||
const ddlTabContentSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/layout/ContentArea.vue"), "utf8");
|
||||
const editorGroupSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/layout/EditorGroup.vue"), "utf8");
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
@@ -57,4 +63,35 @@ describe("DdlViewDialog legacy fallback selectors", () => {
|
||||
expect(content!.hasAttribute(scopeId!)).toBe(false);
|
||||
expect(footer!.hasAttribute(scopeId!)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the DDL dialog free of per-dialog legacy fallback rules", () => {
|
||||
// The dialog content element is rendered through reka-ui's portal Teleport and
|
||||
// never carries this component's scoped data-v attribute, so per-dialog rules
|
||||
// are avoided entirely: width comes from the global sm:max-w-190 legacy entry
|
||||
// and the footer layout from the global dialog-footer rule.
|
||||
expect(ddlViewDialogSource).not.toContain("dbx-legacy-webview");
|
||||
expect(ddlViewDialogSource).not.toContain("@media");
|
||||
expect(ddlViewDialogSource).toContain('class="dbx-ddl-view-dialog flex min-h-0 flex-col overflow-hidden sm:max-w-190"');
|
||||
expect(ddlViewDialogSource).toContain("ddlDisplayOriginal");
|
||||
expect(ddlViewDialogSource).toContain("resizeDdlDialog");
|
||||
});
|
||||
|
||||
it("keeps the dialog as the default and offers opening the DDL in a new tab", () => {
|
||||
expect(ddlViewDialogSource).toContain('import { useQueryStore } from "@/stores/queryStore";');
|
||||
expect(ddlViewDialogSource).toContain("queryStore.createTab(props.connectionId, props.database");
|
||||
expect(ddlViewDialogSource).toContain("sourceView: true");
|
||||
expect(ddlViewDialogSource).toContain('ddlOpenMode === "tab"');
|
||||
expect(ddlViewDialogSource).toContain("ddlViewer = {");
|
||||
expect(ddlViewDialogSource).toContain("updateEditorSettings({ ddlOpenMode: value })");
|
||||
expect(ddlViewDialogSource).toContain('@click="openDdlInNewTab"');
|
||||
});
|
||||
|
||||
it("renders DDL tabs read-only with only refresh and table-data actions", () => {
|
||||
expect(editorGroupSource).toContain("!activeTab.value.ddlViewer");
|
||||
expect(ddlTabContentSource).toContain('v-if="activeTab.ddlViewer" class="flex h-10');
|
||||
expect(ddlTabContentSource).toContain('@click="refreshDdlViewer"');
|
||||
expect(ddlTabContentSource).toContain('@click="viewDdlTableData"');
|
||||
expect(ddlTabContentSource).toContain(':read-only="!!activeTab.ddlViewer"');
|
||||
expect(ddlTabContentSource).toContain(':hide-execution-controls="!!activeTab.ddlViewer"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user