fix(gbase8s): make view source saves safer

This commit is contained in:
zipg authored and GitHub committed 2026-07-07 01:50:53 +08:00
1 parent 15b86ad6c9
commit 668919a7c9
25 files changed
+1343 -129

No files matched your search

@@ -77,6 +77,7 @@ const props = defineProps<{
executionErrorSql?: string;
readOnly?: boolean;
forceWordWrap?: boolean;
hideExecutionControls?: boolean;
initialViewport?: { scrollTop: number; scrollLeft: number };
initialSelection?: { anchor: number; head: number };
}>();
@@ -794,13 +795,17 @@ function selectSqlLineFromGutter(currentView: EditorViewType, line: { from: numb
const contextMenuItems = computed<ContextMenuItem[]>(() => {
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
return [
{
label: executeContextMenuLabel.value,
action: executeFromContextMenu,
disabled: !canExecuteContextSql.value,
icon: Play,
shortcut: shortcuts.executeSql,
},
...(props.hideExecutionControls
? []
: [
{
label: executeContextMenuLabel.value,
action: executeFromContextMenu,
disabled: !canExecuteContextSql.value,
icon: Play,
shortcut: shortcuts.executeSql,
},
]),
{
label: t("contextMenu.viewData"),
action: openTableFromContextMenu,
@@ -849,6 +854,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
const Prec = codeMirrorPrec;
const binding = (shortcut: string, run: (view: EditorViewType) => boolean) => (shortcut ? [{ key: shortcutToCodeMirrorKey(shortcut), preventDefault: true, run }] : []);
const executeBindings = props.hideExecutionControls ? [] : binding(shortcuts.executeSql, () => requestExecute({ forceCurrent: true }));
return [
Prec?.high(
codeMirrorKeymap.of([
@@ -859,7 +865,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
},
...binding(shortcuts.find, openSearch),
...binding(shortcuts.replace, openReplace),
...binding(shortcuts.executeSql, () => requestExecute({ forceCurrent: true })),
...executeBindings,
...binding(shortcuts.saveSql, () => {
emit("save");
return true;
@@ -2643,7 +2649,7 @@ onMounted(async () => {
return { dom };
},
}),
runGutterComp.of(buildRunStatementGutterExtension()),
runGutterComp.of(props.hideExecutionControls ? [] : buildRunStatementGutterExtension()),
lineNumbers({
domEventHandlers: {
mousedown: selectSqlLineFromGutter,
@@ -3042,7 +3048,7 @@ watch(
codeMirrorTheme.reconfigure(themeExt),
wordWrapComp.reconfigure(props.forceWordWrap || ss.wordWrap ? editorViewModule.EditorView.lineWrapping : []),
vimModeComp.reconfigure(vimModeExtension(settingsStore.editorSettings.vimModeEnabled)),
runGutterComp.reconfigure(buildRunStatementGutterExtension?.() ?? []),
runGutterComp.reconfigure(props.hideExecutionControls ? [] : (buildRunStatementGutterExtension?.() ?? [])),
runKeymapComp.reconfigure(runKeymapExtension(editorViewModule.keymap)),
],
});
@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
import type { EditorView } from "@codemirror/view";
import type { ObjectSourceKind } from "@/types/database";
const props = withDefaults(
defineProps<{
@@ -22,6 +23,7 @@ const props = withDefaults(
database: string;
schema?: string;
tableName: string;
objectType?: ObjectSourceKind;
/** SQL dialect for syntax highlighting. Non-PG/non-MSSQL databases fall back to MySQL (same as QueryEditor's source viewer). */
dialect: "mysql" | "postgres" | "sqlserver";
/** SQL formatter dialect. Kept separate from the syntax-highlighting dialect because several PG-compatible DBs highlight as MySQL. */
@@ -56,7 +58,7 @@ watch(
ddlLoading.value = true;
try {
const schema = props.schema || props.database;
const ddl = await api.getTableDdl(props.connectionId, props.database, schema, props.tableName);
const ddl = await api.getTableDdl(props.connectionId, props.database, schema, props.tableName, props.objectType);
ddlContent.value = await formatSqlForDisplay(ddl, props.formatDialect ?? props.dialect, settingsStore.editorSettings.sqlFormatter);
} catch (e: any) {
ddlError.value = e?.message || String(e);
@@ -171,7 +173,7 @@ function retry() {
ddlContent.value = "";
const schema = props.schema || props.database;
api
.getTableDdl(props.connectionId, props.database, schema, props.tableName)
.getTableDdl(props.connectionId, props.database, schema, props.tableName, props.objectType)
.then(async (ddl) => {
ddlContent.value = await formatSqlForDisplay(ddl, props.formatDialect ?? props.dialect, settingsStore.editorSettings.sqlFormatter);
})
@@ -54,9 +54,8 @@ import { codeMirrorSqlDialect, connectionUsesDatabaseObjectTreeMode, effectiveDa
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
import { useToast } from "@/composables/useToast";
import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, objectSourceSaveExecutionMode, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, executeObjectSourceSave, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRenameSql";
import { buildViewDdl } from "@/lib/table/viewDdl";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
import { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/common/clipboard";
@@ -123,6 +122,7 @@ const sourceContent = ref("");
const sourceError = ref("");
const sourceRow = ref<ObjectBrowserRow | null>(null);
const sourceEditing = ref(false);
const sourceCanEdit = ref(true);
const effectiveDatabaseType = computed(() => effectiveDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
const tableStructureDatabaseType = computed(() => tableStructureDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
const sourceEditableText = ref("");
@@ -426,12 +426,14 @@ async function openSource(row: ObjectBrowserRow) {
sourceContent.value = "";
sourceError.value = "";
sourceEditing.value = false;
sourceCanEdit.value = true;
sourceEditableText.value = "";
sourceDraft.value = "";
sourceSaveError.value = "";
sourceLoading.value = true;
try {
const result = await api.getObjectSource(props.connection.id, props.database, row.schema || selectedSchema.value || props.database, row.name, row.type as ObjectSourceKind);
sourceCanEdit.value = result.editable !== false && row.type !== "SEQUENCE";
const editable = await api.buildEditableObjectSource({
databaseType: effectiveDatabaseType.value,
objectType: row.type as ObjectSourceKind,
@@ -442,7 +444,10 @@ async function openSource(row: ObjectBrowserRow) {
sourceEditableText.value = editable;
sourceContent.value = await formatSqlForDisplay(editable, sourceFormatDialect.value, settingsStore.editorSettings.sqlFormatter);
sourceDraft.value = editable;
sourceEditing.value = row.type !== "SEQUENCE";
sourceEditing.value = sourceCanEdit.value;
if (!sourceCanEdit.value && row.type !== "SEQUENCE") {
toast(t("objects.sourceReadOnly"), 3000);
}
} catch (e: any) {
sourceError.value = e?.message || String(e);
} finally {
@@ -450,27 +455,6 @@ async function openSource(row: ObjectBrowserRow) {
}
}
async function openViewDdl(row: ObjectBrowserRow) {
if (row.type !== "VIEW" && row.type !== "MATERIALIZED_VIEW") return;
try {
const schema = row.schema || selectedSchema.value || props.database;
const ddl =
row.type === "MATERIALIZED_VIEW"
? await api.getTableDdl(props.connection.id, props.database, schema, row.name, "MATERIALIZED_VIEW")
: await buildViewDdl({
databaseType: effectiveDatabaseType.value,
schema,
name: row.name,
source: (await api.getObjectSource(props.connection.id, props.database, schema, row.name, "VIEW")).source,
});
const formatted = await formatSqlForDisplay(ddl, sourceFormatDialect.value, settingsStore.editorSettings.sqlFormatter);
const tabId = queryStore.createTab(props.connection.id, props.database, `DDL - ${row.name}`);
queryStore.updateSql(tabId, formatted);
} catch (e: any) {
toast(e?.message || String(e), 5000);
}
}
async function openNewQuery(row: ObjectBrowserRow) {
const tabId = queryStore.createTab(props.connection.id, props.database, row.name);
queryStore.updateSql(
@@ -1219,6 +1203,10 @@ async function copySource() {
function editSource() {
if (!sourceRow.value || !sourceEditableText.value) return;
if (!sourceCanEdit.value) {
toast(t("objects.sourceReadOnly"), 3000);
return;
}
sourceDraft.value = sourceEditableText.value;
sourceSaveError.value = "";
sourceEditing.value = true;
@@ -1231,6 +1219,10 @@ function cancelEditSource() {
}
async function saveSource() {
if (!sourceCanEdit.value) {
toast(t("objects.sourceReadOnly"), 3000);
return;
}
if (!sourceRow.value || !sourceDraft.value.trim()) return;
const row = sourceRow.value;
const schema = row.schema || selectedSchema.value || props.database;
@@ -1244,13 +1236,7 @@ async function saveSource() {
name: row.name,
source: sourceDraft.value,
});
for (const sql of statements) {
if (objectSourceSaveExecutionMode(effectiveDatabaseType.value) === "single") {
await api.executeQuery(props.connection.id, props.database, sql, schema);
} else {
await api.executeScript(props.connection.id, props.database, sql, schema);
}
}
await executeObjectSourceSave(props.connection.id, props.database, effectiveDatabaseType.value, statements, schema);
toast(t("objects.sourceSaved"));
sourceEditing.value = false;
sourceDraft.value = "";
@@ -1502,7 +1488,14 @@ function getViewMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
{ label: t("contextMenu.viewData"), action: () => openViewData(item), icon: Table2 },
{ label: t("contextMenu.editView"), action: () => openSource(item), icon: PencilLine },
{ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 },
{ label: t("contextMenu.viewDdl"), action: () => openViewDdl(item), icon: ScrollText },
{
label: t("contextMenu.viewDdl"),
action: () => {
ddlDialogTarget.value = item;
showDdlDialog.value = true;
},
icon: ScrollText,
},
...(canRename(item) ? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }] : []),
{ label: t("contextMenu.newQuery"), action: () => openNewQuery(item), icon: TerminalSquare },
...(canOpenDiagram.value ? [{ label: t("diagram.open"), action: () => openDiagram(item), icon: Network }] : []),
@@ -1805,7 +1798,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<Button v-if="!sourceEditing" variant="ghost" size="icon" class="h-5 w-5" :disabled="!sourceContent" @click="copySource">
<Copy class="h-3 w-3" />
</Button>
<Button v-if="!sourceEditing" variant="ghost" size="icon" class="h-5 w-5" :disabled="!sourceContent" @click="editSource">
<Button v-if="!sourceEditing && sourceCanEdit" variant="ghost" size="icon" class="h-5 w-5" :disabled="!sourceContent" @click="editSource">
<PencilLine class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="closeSource">
@@ -1949,7 +1942,17 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
</DialogContent>
</Dialog>
<DdlViewDialog v-if="ddlDialogTarget" :connection-id="props.connection.id" :database="props.database" :schema="ddlDialogTarget.schema || selectedSchema" :table-name="ddlDialogTarget.name" :dialect="sourceDialect" :format-dialect="sourceFormatDialect" v-model:open="showDdlDialog" />
<DdlViewDialog
v-if="ddlDialogTarget"
:connection-id="props.connection.id"
:database="props.database"
:schema="ddlDialogTarget.schema || selectedSchema"
:table-name="ddlDialogTarget.name"
:object-type="tableDdlObjectType(ddlDialogTarget.type)"
:dialect="sourceDialect"
:format-dialect="sourceFormatDialect"
v-model:open="showDdlDialog"
/>
</template>
<style scoped>
@@ -0,0 +1,247 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Clipboard, Loader2, PencilLine, RefreshCw } from "@lucide/vue";
import { useToast } from "@/composables/useToast";
import { useSettingsStore } from "@/stores/settingsStore";
import { copyToClipboard } from "@/lib/common/clipboard";
import { formatSqlForDisplay, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { buildEditableObjectSource, buildExecutableObjectSourceStatements, executeObjectSourceSave } from "@/lib/table/objectSourceEditor";
import * as api from "@/lib/backend/api";
import QueryEditor from "@/components/editor/QueryEditor.vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import type { DatabaseType, ObjectSourceKind } from "@/types/database";
const props = withDefaults(
defineProps<{
open: boolean;
connectionId: string;
database: string;
schema?: string;
name: string;
objectType: ObjectSourceKind;
databaseType?: DatabaseType;
dialect: "mysql" | "postgres" | "sqlserver";
formatDialect?: SqlFormatDialect;
initialEditing?: boolean;
}>(),
{
initialEditing: false,
},
);
const emit = defineEmits<{
"update:open": [value: boolean];
saved: [];
}>();
const { t } = useI18n();
const { toast } = useToast();
const settingsStore = useSettingsStore();
const content = ref("");
const editableText = ref("");
const draft = ref("");
const loading = ref(false);
const saving = ref(false);
const editing = ref(false);
const sourceEditable = ref(true);
const error = ref("");
const saveError = ref("");
let loadSerial = 0;
const canEdit = computed(() => sourceEditable.value && props.objectType !== "SEQUENCE");
const title = computed(() => `${editing.value ? t("contextMenu.editView") : t("contextMenu.viewSource")} - ${props.name}`);
watch(
() => [props.open, props.connectionId, props.database, props.schema, props.name, props.objectType, props.initialEditing] as const,
() => {
if (props.open) void loadSource();
},
{ immediate: true },
);
async function loadSource(nextEditing = props.initialEditing && canEdit.value) {
const serial = ++loadSerial;
content.value = "";
editableText.value = "";
draft.value = "";
error.value = "";
saveError.value = "";
sourceEditable.value = true;
editing.value = false;
loading.value = true;
try {
if (!props.databaseType) throw new Error("Connection type is unavailable.");
const schema = props.schema || props.database;
const result = await api.getObjectSource(props.connectionId, props.database, schema, props.name, props.objectType);
const editableAllowed = result.editable !== false;
const editable = await buildEditableObjectSource({
databaseType: props.databaseType,
objectType: props.objectType,
schema,
name: props.name,
source: result.source,
});
if (serial !== loadSerial) return;
sourceEditable.value = editableAllowed;
const formatted = await formatSqlForDisplay(editable, props.formatDialect ?? props.dialect, settingsStore.editorSettings.sqlFormatter);
editableText.value = editable;
content.value = formatted;
draft.value = nextEditing && canEdit.value ? editable : "";
editing.value = nextEditing && canEdit.value;
if (nextEditing && !canEdit.value) {
toast(t("objects.sourceReadOnly"), 3000);
}
} catch (e: any) {
if (serial === loadSerial) error.value = e?.message || String(e);
} finally {
if (serial === loadSerial) loading.value = false;
}
}
async function copySource() {
if (!content.value) return;
try {
await copyToClipboard(content.value);
toast(t("grid.copied"));
} catch (e: any) {
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
}
}
function editSource() {
if (!canEdit.value || !editableText.value) {
if (!canEdit.value) toast(t("objects.sourceReadOnly"), 3000);
return;
}
draft.value = editableText.value;
saveError.value = "";
editing.value = true;
}
function cancelEditSource() {
editing.value = false;
draft.value = "";
saveError.value = "";
}
async function saveSource() {
if (!canEdit.value) {
toast(t("objects.sourceReadOnly"), 3000);
return;
}
if (!draft.value.trim() || !props.databaseType) return;
const schema = props.schema || props.database;
saving.value = true;
saveError.value = "";
try {
const statements = await buildExecutableObjectSourceStatements({
databaseType: props.databaseType,
objectType: props.objectType,
schema,
name: props.name,
source: draft.value,
});
await executeObjectSourceSave(props.connectionId, props.database, props.databaseType, statements, schema);
toast(t("objects.sourceSaved"));
emit("saved");
await loadSource(false);
} catch (e: any) {
saveError.value = e?.message || String(e);
} finally {
saving.value = false;
}
}
function closeDialog() {
emit("update:open", false);
}
</script>
<template>
<Dialog :open="props.open" @update:open="(value) => emit('update:open', value)">
<DialogContent class="h-[min(760px,calc(100dvh-2rem))] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-[900px]">
<DialogHeader>
<DialogTitle>{{ title }}</DialogTitle>
</DialogHeader>
<div v-if="loading" class="flex min-h-0 items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
<span>{{ t("common.loading") }}</span>
</div>
<div v-else-if="error" class="flex min-h-0 flex-col items-center justify-center gap-3 text-sm">
<p class="text-destructive">{{ error }}</p>
<Button variant="outline" size="sm" @click="loadSource()">
<RefreshCw class="h-4 w-4" />
{{ t("common.retry") }}
</Button>
</div>
<div v-else-if="editing" class="object-source-dialog-editor flex min-h-0 flex-col overflow-hidden rounded border" data-object-source-editor>
<QueryEditor
v-model="draft"
class="min-h-0 flex-1"
:connection-id="props.connectionId"
:database="props.database"
:schema="props.schema || props.database"
:database-type="props.databaseType"
:dialect="props.dialect"
:format-dialect="props.formatDialect"
force-word-wrap
hide-execution-controls
@save="saveSource"
/>
<div v-if="saveError" class="shrink-0 border-t px-3 py-2 text-xs text-destructive">
{{ saveError }}
</div>
</div>
<QueryEditor
v-else
:key="`${props.connectionId}:${props.database}:${props.schema || ''}:${props.name}:${props.objectType}`"
:model-value="content"
class="object-source-dialog-editor min-h-0 overflow-hidden rounded border"
:connection-id="props.connectionId"
:database="props.database"
:schema="props.schema || props.database"
:database-type="props.databaseType"
:dialect="props.dialect"
:format-dialect="props.formatDialect"
force-word-wrap
read-only
hide-execution-controls
data-object-source-preview
/>
<DialogFooter>
<Button variant="outline" @click="closeDialog">{{ t("common.close") }}</Button>
<Button v-if="!editing" variant="outline" :disabled="!content" @click="copySource">
<Clipboard class="h-4 w-4" />
{{ t("grid.copy") }}
</Button>
<Button v-if="!editing && canEdit" variant="outline" :disabled="!editableText" @click="editSource">
<PencilLine class="h-4 w-4" />
{{ t("contextMenu.editView") }}
</Button>
<Button v-if="editing" variant="outline" :disabled="saving" @click="cancelEditSource">
{{ t("objects.cancelEdit") }}
</Button>
<Button v-if="editing" :disabled="saving || !draft.trim()" @click="saveSource">
<Loader2 v-if="saving" class="h-4 w-4 animate-spin" />
{{ t("objects.saveSource") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<style scoped>
.object-source-dialog-editor :deep(.cm-editor),
.object-source-dialog-editor :deep(.cm-scroller) {
height: 100%;
}
.object-source-dialog-editor :deep(.cm-scroller) {
overflow: auto !important;
}
</style>
@@ -126,10 +126,11 @@ import {
type TableAdminSqlOptions,
} from "@/lib/database/dbAdminSql";
import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType } from "@/lib/table/objectRenameSql";
import { buildEditableObjectSource, buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
import { buildViewDdl } from "@/lib/table/viewDdl";
import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
import DdlViewDialog from "@/components/objects/DdlViewDialog.vue";
import ObjectSourceDialog from "@/components/objects/ObjectSourceDialog.vue";
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
import { codeMirrorSqlDialect, connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { hexToRgba } from "@/lib/common/color";
@@ -709,7 +710,7 @@ function runRowClickAction(clickDetail: number) {
} else if (isDocumentBrowserTreeNode(node.type)) {
openMongoTreeData(node);
} else if (node.type === "procedure" || node.type === "function" || node.type === "sequence" || node.type === "package" || node.type === "package-body") {
void viewObjectSource();
openObjectSourceDialog(false);
} else if (action === "toggle") {
toggle();
}
@@ -1041,7 +1042,7 @@ function onDoubleClick() {
} else if (action === "open-data") {
openData();
} else if (action === "open-source") {
void viewObjectSource();
openObjectSourceDialog(false);
} else if (action === "open-saved-sql") {
openSavedSqlFile();
} else if (action === "toggle" && (props.node.type === "mongo-gridfs" || isDocumentBrowserTreeNode(props.node.type))) {
@@ -1747,6 +1748,15 @@ const ddlFormatDialect = computed(() => {
if (!ddlTarget.value?.connectionId) return "generic";
return sqlFormatDialectForDbType(effectiveDatabaseTypeForConnection(connectionStore.getConfig(ddlTarget.value.connectionId)));
});
const objectSourceTarget = ref<{ node: TreeNode; initialEditing: boolean } | null>(null);
const showObjectSourceDialog = ref(false);
const objectSourceType = computed(() => (objectSourceTarget.value ? objectSourceKindForTreeNode(objectSourceTarget.value.node.type) : null));
const objectSourceDatabaseType = computed(() => {
const connectionId = objectSourceTarget.value?.node.connectionId;
return connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(connectionId)) : undefined;
});
const objectSourceDialect = computed(() => codeMirrorSqlDialect(objectSourceDatabaseType.value));
const objectSourceFormatDialect = computed(() => sqlFormatDialectForDbType(objectSourceDatabaseType.value));
const showCreateDatabaseDialog = ref(false);
const createDatabaseName = ref("");
const createDatabaseCharset = ref("utf8mb4");
@@ -1917,66 +1927,17 @@ async function refreshDropTableChildObjectPreviewSql() {
dropTableChildObjectPreviewSql.value = options ? await buildDropTableChildObjectSql(options).catch(() => "") : "";
}
function viewObjectSource() {
function openObjectSourceDialog(initialEditing: boolean) {
const node = props.node;
if (!node.connectionId || !node.database) return;
const objectType = objectSourceKindForTreeNode(node.type);
if (!objectType) return;
const schema = node.schema || node.database;
connectionStore
void connectionStore
.ensureConnected(node.connectionId)
.then(() => {
connectionStore.activeConnectionId = node.connectionId!;
return api.getObjectSource(node.connectionId!, node.database!, schema, node.label, objectType as any);
})
.then(async (result) => {
const databaseType = currentDatabaseType();
if (!databaseType) throw new Error("Connection type is unavailable.");
const tabId = queryStore.createTab(node.connectionId!, node.database!, `Source - ${node.label}`);
const editable = await buildEditableObjectSource({
databaseType,
objectType,
schema,
name: node.label,
source: result.source,
});
queryStore.updateSql(tabId, editable);
if (objectType !== "SEQUENCE") {
queryStore.setObjectSource(tabId, {
schema,
name: node.label,
objectType,
});
}
queryStore.markTabClean(queryStore.tabs.find((tab) => tab.id === tabId));
})
.catch((e: any) => {
toast(e?.message || String(e), 5000);
});
}
function viewObjectDdl() {
const node = props.node;
if ((node.type !== "view" && node.type !== "materialized_view") || !node.connectionId || !node.database) return;
const schema = node.schema || node.database;
const objectType = node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "VIEW";
connectionStore
.ensureConnected(node.connectionId)
.then(() => {
connectionStore.activeConnectionId = node.connectionId!;
return api.getObjectSource(node.connectionId!, node.database!, schema, node.label, objectType);
})
.then(async (result) => {
const connection = connectionStore.getConfig(node.connectionId!);
const ddl = await buildViewDdl({
databaseType: effectiveDatabaseTypeForConnection(connection),
schema,
name: node.label,
source: result.source,
});
const formatted = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(effectiveDatabaseTypeForConnection(connection)), settingsStore.editorSettings.sqlFormatter);
const tabId = queryStore.createTab(node.connectionId!, node.database!, `DDL - ${node.label}`);
queryStore.updateSql(tabId, formatted);
objectSourceTarget.value = { node, initialEditing };
showObjectSourceDialog.value = true;
})
.catch((e: any) => {
toast(e?.message || String(e), 5000);
@@ -3273,9 +3234,16 @@ function createView() {
const node = props.node;
if (!node.connectionId || !node.database) return;
connectionStore.activeConnectionId = node.connectionId;
const viewName = node.schema ? `${node.schema}.new_view` : "new_view";
const viewName = "new_view";
const effectiveDbType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId));
const viewSqlName = effectiveDbType === "informix" || !node.schema ? viewName : `${node.schema}.${viewName}`;
const tabId = queryStore.createTab(node.connectionId, node.database, t("contextMenu.createView"), "query", node.schema);
queryStore.updateSql(tabId, `CREATE VIEW ${viewName} AS\nSELECT\n *\nFROM table_name;\n`);
queryStore.updateSql(tabId, `CREATE VIEW ${viewSqlName} AS\nSELECT\n *\nFROM table_name;\n`);
queryStore.setObjectSource(tabId, {
schema: node.schema,
name: viewName,
objectType: "VIEW",
});
}
async function saveFileContent(content: string, defaultFileName: string, filterName: string, filterExt: string) {
@@ -4697,9 +4665,16 @@ function treeItemMenuItems(): ContextMenuItem[] {
});
}
if (node.type === "view" || node.type === "materialized_view") {
items.push({ label: t("contextMenu.editView"), action: viewObjectSource, icon: Pencil });
items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 });
items.push({ label: t("contextMenu.viewDdl"), action: viewObjectDdl, icon: FileCode });
items.push({ label: t("contextMenu.editView"), action: () => openObjectSourceDialog(true), icon: Pencil });
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
items.push({
label: t("contextMenu.viewDdl"),
action: () => {
ddlTarget.value = node;
showDdlDialog.value = true;
},
icon: FileCode,
});
}
if (canOpenStructureEditor.value) {
items.push({ label: t("contextMenu.editStructure"), action: openStructureEditor, icon: PencilRuler });
@@ -4853,7 +4828,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (node.type === "procedure") {
items.push({ label: t("contextMenu.executeProcedure"), action: openProcedureExecution, icon: Play });
}
items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 });
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
if (canRenameObject.value) {
items.push({
label: t("contextMenu.renameObject"),
@@ -4874,14 +4849,14 @@ function treeItemMenuItems(): ContextMenuItem[] {
}
if (node.type === "sequence") {
items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 });
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
return items;
}
if (node.type === "package" || node.type === "package-body") {
items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 });
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
return items;
@@ -5561,7 +5536,32 @@ function treeItemMenuItems(): ContextMenuItem[] {
<DangerConfirmDialog v-model:open="showDropSchemaConfirm" :title="t('contextMenu.confirmDropSchemaTitle')" :message="t('contextMenu.confirmDropSchemaMessage', { name: node.label })" :sql="dropSchemaPreviewSql" :confirm-label="t('contextMenu.dropSchema')" @confirm="confirmDropSchema" />
<DdlViewDialog v-if="ddlTarget" :connection-id="ddlTarget.connectionId!" :database="ddlTarget.database!" :schema="ddlTarget.schema" :table-name="ddlTarget.label" :dialect="ddlDialect" :format-dialect="ddlFormatDialect" v-model:open="showDdlDialog" />
<DdlViewDialog
v-if="ddlTarget"
:connection-id="ddlTarget.connectionId!"
:database="ddlTarget.database!"
:schema="ddlTarget.schema"
:table-name="ddlTarget.label"
:object-type="tableDdlObjectTypeForNode(ddlTarget.type)"
:dialect="ddlDialect"
:format-dialect="ddlFormatDialect"
v-model:open="showDdlDialog"
/>
<ObjectSourceDialog
v-if="objectSourceTarget && objectSourceType"
v-model:open="showObjectSourceDialog"
:connection-id="objectSourceTarget.node.connectionId!"
:database="objectSourceTarget.node.database!"
:schema="objectSourceTarget.node.schema"
:name="objectSourceTarget.node.label"
:object-type="objectSourceType"
:database-type="objectSourceDatabaseType"
:dialect="objectSourceDialect"
:format-dialect="objectSourceFormatDialect"
:initial-editing="objectSourceTarget.initialEditing"
@saved="refresh"
/>
<InstallExtensionDialog ref="installExtensionDialogRef" :node="node" @close="refresh" />
</template>