fix(mysql): prevalidate routine saves and support procedure refresh
This commit is contained in:
1 parent
0023c03520
commit
4ac2c5dc40
16 files changed
+173
-20
No files matched your search
@@ -1035,6 +1035,10 @@ function refreshData(): boolean {
|
||||
void pluginFilesystemTabRef.value?.refresh();
|
||||
return true;
|
||||
}
|
||||
if (props.activeTab.objectSource || props.activeTab.sourceLoad) {
|
||||
if (queryStore.isTabDirty(props.activeTab) && !window.confirm(t("objects.refreshDiscardConfirm"))) return false;
|
||||
return queryStore.refreshObjectSourceTab(props.activeTab.id);
|
||||
}
|
||||
// Restored data tabs intentionally omit row data, so refresh must work before DataGrid mounts.
|
||||
if (canReloadUnavailableDataTab(props.activeTab)) {
|
||||
reloadUnavailableDataTab();
|
||||
|
||||
@@ -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, WrapText } 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, RefreshCw } from "@lucide/vue";
|
||||
import { supportsInsertValueHints } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
@@ -12,6 +12,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import ConnectionTreeSelect from "@/components/connection/ConnectionTreeSelect.vue";
|
||||
import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { catalogDatabaseOptionsKey, databaseAfterCatalogChange, normalizedQueryTabCatalog, queryCatalogSelectorVisible, selectedQueryCatalogName, useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
||||
import { useSchemaOptions } from "@/composables/useSchemaOptions";
|
||||
@@ -92,6 +93,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOptions, loadingCatalogOptions, loadCatalogOptions, catalogDatabaseOptions, loadingCatalogDatabaseOptions, loadCatalogDatabaseOptions } = useDatabaseOptions();
|
||||
const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas, isSchemaAware } = useSchemaOptions();
|
||||
@@ -244,6 +246,14 @@ const saveTooltip = computed(() => {
|
||||
if (props.activeTab.externalSqlPath) return t("toolbar.saveSqlFile");
|
||||
return t("toolbar.saveSql");
|
||||
});
|
||||
const isObjectSourceTab = computed(() => !!props.activeTab.objectSource || !!props.activeTab.sourceLoad);
|
||||
const objectSourceRefreshing = computed(() => !!props.activeTab.sourceLoad && !props.activeTab.sourceLoad.error);
|
||||
|
||||
function refreshObjectSource() {
|
||||
if (!isObjectSourceTab.value) return;
|
||||
if (queryStore.isTabDirty(props.activeTab) && !window.confirm(t("objects.refreshDiscardConfirm"))) return;
|
||||
queryStore.refreshObjectSourceTab(props.activeTab.id);
|
||||
}
|
||||
const executeShortcutDisplay = computed(() => formatShortcutDisplay(settingsStore.editorSettings.shortcuts.executeSql));
|
||||
const executeShortcutTooltip = computed(() => t("toolbar.executeShortcut", { shortcut: executeShortcutDisplay.value }));
|
||||
// executableSql 在无选区时可能是整篇文档;只要有 DML 语句出现就显示预览按钮,
|
||||
@@ -606,6 +616,22 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ saveTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="isObjectSourceTab">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 text-emerald-600 hover:bg-emerald-500/10 hover:text-emerald-700 dark:text-emerald-300 dark:hover:text-emerald-200"
|
||||
:disabled="objectSourceRefreshing || activeTab.isExecuting"
|
||||
:aria-label="t('structureEditor.refresh')"
|
||||
@click="refreshObjectSource"
|
||||
>
|
||||
<Loader2 v-if="objectSourceRefreshing" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCw v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("structureEditor.refresh") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="showOpenSqlButton">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-sky-600 hover:bg-sky-500/10 hover:text-sky-700 dark:text-sky-300 dark:hover:text-sky-200" @click="emit('openSql')">
|
||||
|
||||
@@ -1385,6 +1385,10 @@ async function fetchTableConstraints(force = false) {
|
||||
}
|
||||
|
||||
async function refreshActiveTableInfo() {
|
||||
if (sidePanelMode.value === "source") {
|
||||
await refreshActiveSource();
|
||||
return;
|
||||
}
|
||||
if (sidePanelMode.value !== "table-info" || !sidePanelRow.value) return;
|
||||
sidePanelGuard.bump();
|
||||
|
||||
@@ -1502,9 +1506,14 @@ async function openSource(row: ObjectBrowserRow) {
|
||||
closeSidePanel();
|
||||
return;
|
||||
}
|
||||
await loadSourcePanel(row);
|
||||
}
|
||||
|
||||
async function loadSourcePanel(row: ObjectBrowserRow, options?: { preserveEditing?: boolean }) {
|
||||
// Starting a different object must invalidate slower source requests before
|
||||
// any state is reset, otherwise an old response can populate the new row.
|
||||
const epoch = sidePanelGuard.start();
|
||||
const preserveEditing = options?.preserveEditing === true && sourceEditing.value;
|
||||
sidePanelRow.value = row;
|
||||
sidePanelMode.value = "source";
|
||||
sourceRow.value = row;
|
||||
@@ -1538,7 +1547,9 @@ async function openSource(row: ObjectBrowserRow) {
|
||||
sourceEditableText.value = editable;
|
||||
sourceContent.value = row.type === "SEQUENCE" ? result.source : editable;
|
||||
sourceDraft.value = editable;
|
||||
sourceEditing.value = sourceCanEdit.value && row.type !== "SEQUENCE";
|
||||
// Fresh open always enters edit when allowed; refresh preserves prior edit mode.
|
||||
const enterEditing = sourceCanEdit.value && row.type !== "SEQUENCE" && (options?.preserveEditing ? preserveEditing : true);
|
||||
sourceEditing.value = enterEditing;
|
||||
if (!sourceCanEdit.value && row.type !== "SEQUENCE") {
|
||||
toast(t("objects.sourceReadOnly"), 3000);
|
||||
}
|
||||
@@ -1550,6 +1561,13 @@ async function openSource(row: ObjectBrowserRow) {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshActiveSource() {
|
||||
const row = sourceRow.value || (sidePanelMode.value === "source" ? sidePanelRow.value : null);
|
||||
if (!row || sidePanelMode.value !== "source") return;
|
||||
if (sourceEditing.value && !window.confirm(t("objects.refreshDiscardConfirm"))) return;
|
||||
await loadSourcePanel(row, { preserveEditing: true });
|
||||
}
|
||||
|
||||
function openEventEditor(row: ObjectBrowserRow) {
|
||||
sidePanelGuard.start();
|
||||
sidePanelRow.value = row;
|
||||
@@ -4039,6 +4057,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<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" :disabled="sourceLoading || sourceSaving" :title="t('structureEditor.refresh')" :aria-label="t('structureEditor.refresh')" @click="refreshActiveSource">
|
||||
<RefreshCw class="h-3 w-3" :class="{ 'animate-spin': sourceLoading }" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click="closeSource">
|
||||
<X class="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
@@ -156,6 +156,12 @@ function cancelEditSource() {
|
||||
saveError.value = "";
|
||||
}
|
||||
|
||||
async function refreshSource() {
|
||||
if (loading.value || saving.value) return;
|
||||
if (editing.value && !window.confirm(t("objects.refreshDiscardConfirm"))) return;
|
||||
await loadSource(editing.value && canEdit.value);
|
||||
}
|
||||
|
||||
async function saveSource() {
|
||||
if (saving.value || loading.value || !props.open) return;
|
||||
if (!canEdit.value) {
|
||||
@@ -271,6 +277,10 @@ function closeDialog() {
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="closeDialog">{{ t("common.close") }}</Button>
|
||||
<Button variant="outline" :disabled="loading || saving" :title="t('structureEditor.refresh')" @click="refreshSource">
|
||||
<RefreshCw class="h-4 w-4" />
|
||||
{{ t("structureEditor.refresh") }}
|
||||
</Button>
|
||||
<Button v-if="!editing" variant="outline" :disabled="!content" @click="copySource">
|
||||
<Clipboard class="h-4 w-4" />
|
||||
{{ t("grid.copy") }}
|
||||
|
||||
@@ -1395,7 +1395,7 @@ function requestRefreshSelectedNode(): boolean {
|
||||
|
||||
function canRefreshTreeNodeShortcut(): boolean {
|
||||
const type = activeNode.value.type;
|
||||
if (type === "connection" || type === "database" || type === "schema" || type === "table" || type === "view") {
|
||||
if (type === "connection" || type === "database" || type === "schema" || type === "table" || type === "view" || type === "procedure" || type === "function") {
|
||||
return true;
|
||||
}
|
||||
return isGroupLabel(activeNode.value) && type !== "group-partitions";
|
||||
@@ -6394,6 +6394,12 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
||||
items.push({
|
||||
label: t("contextMenu.refreshChildren"),
|
||||
action: refresh,
|
||||
icon: RefreshCw,
|
||||
shortcut: shortcutRefresh,
|
||||
});
|
||||
if (!isPackageMember) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
|
||||
Reference in new issue
Block a user