diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index d6576c2ba..34bafd8f0 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -217,6 +217,26 @@ function openAiPanel() { } } +function isActiveObjectSourceReadOnly() { + return !!activeTab.value?.objectSource?.readOnlyReason; +} + +function guardWritableObjectSource() { + if (!isActiveObjectSourceReadOnly()) return true; + toast(t("objects.sourceReadOnlySystemObject"), 5000); + return false; +} + +function tryExecuteActiveTab() { + if (!guardWritableObjectSource()) return; + tryExecute(); +} + +function tryExplainActiveTab() { + if (!guardWritableObjectSource()) return; + tryExplain(); +} + function analyzeHistoryWithAi(entry: HistoryEntry) { const connectionId = entry.connection_id || activeTab.value?.connectionId; if (!connectionId) { @@ -253,6 +273,7 @@ async function openSaveSqlDialog() { const tab = activeTab.value; if (!tab || !tab.sql.trim()) return; if (tab.objectSource) { + if (!guardWritableObjectSource()) return; await saveActiveObjectSource(tab); return; } @@ -282,6 +303,10 @@ async function saveActiveObjectSource(tab: NonNullable) const connection = connectionStore.getConfig(tab.connectionId); const source = tab.objectSource; if (!connection || !source) return; + if (source.readOnlyReason) { + toast(t("objects.sourceReadOnlySystemObject"), 5000); + return; + } try { const statements = buildExecutableObjectSourceStatements({ @@ -563,7 +588,7 @@ function handleKeydown(e: KeyboardEvent) { ) { e.preventDefault(); e.stopPropagation(); - tryExecute(); + tryExecuteActiveTab(); } } @@ -757,9 +782,9 @@ onUnmounted(() => { :active-tab="activeTab" :active-connection="activeConnection" :executable-sql="executableSql" - @execute="tryExecute()" + @execute="tryExecuteActiveTab" @cancel="cancelActiveExecution()" - @explain="tryExplain()" + @explain="tryExplainActiveTab" @format-sql="formatActiveSql" @save-sql="void openSaveSqlDialog()" @open-sql="openSqlFile" @@ -780,9 +805,9 @@ onUnmounted(() => { :cursor-pos="cursorPos" @update:active-output-view="activeOutputView = $event" @fix-with-ai="fixWithAi" - @execute="tryExecute()" + @execute="tryExecuteActiveTab" @cancel="cancelActiveExecution()" - @explain="tryExplain()" + @explain="tryExplainActiveTab" @editor-update=" (v: string) => { if (queryStore.activeTabId) queryStore.updateSql(queryStore.activeTabId, v); diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 5c59a2af8..c83a101c5 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -28,6 +28,7 @@ const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explai const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue")); import { useQueryStore } from "@/stores/queryStore"; import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/queryExecutionState"; +import { shouldShowQueryOutputPane } from "@/lib/contentAreaLayout"; import { databaseDisplayNameForTab } from "@/lib/tabPresentation"; import { isTableDataEditable } from "@/lib/tableEditing"; import type { QueryTab, ConnectionConfig } from "@/types/database"; @@ -114,6 +115,9 @@ const activeQueryError = computed(() => { return String(result.rows[0]?.[0] ?? ""); }); +const showQueryOutputPane = computed(() => shouldShowQueryOutputPane(props.activeTab)); +const objectSourceReadOnly = computed(() => !!props.activeTab.objectSource?.readOnlyReason); + // Column info panel handlers async function onHandleClickColumn( matchedCols: Array<{ name: string; table: string; schema?: string }>, @@ -202,7 +206,7 @@ defineExpose({ focusSearch }); @@ -701,7 +743,8 @@ watch( variant="ghost" size="icon" class="h-5 w-5" - :disabled="!sourceContent" + :disabled="!sourceContent || !!sourceRowReadOnlyReason" + :title="sourceRowReadOnlyReason ? t('objects.sourceReadOnlySystemObject') : undefined" @click="editSource" > @@ -724,6 +767,7 @@ watch( :database="props.database" :dialect="sourceDialect" :format-dialect="sourceFormatDialect" + :read-only="!!sourceRowReadOnlyReason" force-word-wrap @save="saveSource" /> diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 326f8784c..b96417b01 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -92,6 +92,7 @@ import { treeNodeRowAction, treeNodeRowDoubleClickAction, } from "@/lib/treeNodeClick"; +import { objectSourceReadOnlyReason } from "@/lib/objectSourceEditor"; import { formatCsv, formatJson, formatSqlInsert } from "@/lib/exportFormats"; import { fetchTableDataForExport } from "@/lib/tableDataExport"; import { @@ -659,6 +660,18 @@ function viewObjectSource() { schema, name: node.label, objectType, + readOnlyReason: (() => { + const databaseType = currentDatabaseType(); + if (!databaseType) return undefined; + return ( + objectSourceReadOnlyReason({ + databaseType, + schema, + name: node.label, + objectType, + }) ?? undefined + ); + })(), }); }) .catch((e: any) => { @@ -667,6 +680,10 @@ function viewObjectSource() { } function requestDropObject() { + if (currentObjectSourceReadOnlyReason.value) { + toast(t("objects.sourceReadOnlySystemObject"), 5000); + return; + } showDropObjectConfirm.value = true; } @@ -680,7 +697,21 @@ function nodeRenameObjectType(): RenameableObjectType | null { const canRenameObject = computed(() => { const objectType = nodeRenameObjectType(); - return !!objectType && supportsObjectRename(currentDatabaseType(), objectType); + return ( + !!objectType && !currentObjectSourceReadOnlyReason.value && supportsObjectRename(currentDatabaseType(), objectType) + ); +}); + +const currentObjectSourceReadOnlyReason = computed(() => { + const objectType = objectSourceKindForTreeNode(props.node.type); + const databaseType = currentDatabaseType(); + if (!objectType || !databaseType) return null; + return objectSourceReadOnlyReason({ + databaseType, + schema: props.node.schema, + name: props.node.label, + objectType, + }); }); function openRenameObjectDialog() { @@ -1968,11 +1999,13 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr {{ t("contextMenu.renameObject") }} - - - - {{ node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction") }} - +