feat(ui): refresh latest ddl on demand

This commit is contained in:
zipg authored and GitHub committed 2026-09-01 01:46:39 +08:00
1 parent f9ff9245ce
commit cd008b2d56
17 files changed
+186 -11

No files matched your search

@@ -12,6 +12,8 @@ import { formatSqlForDisplay, type SqlFormatDialect } from "@/lib/sql/sqlFormatt
import { loadObjectDdl } from "@/lib/metadata/objectDdlCache";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";
import { HelpTooltip } from "@/components/ui/tooltip";
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
import type { EditorView } from "@codemirror/view";
import type { DatabaseType, ObjectSourceKind } from "@/types/database";
@@ -131,7 +133,7 @@ async function loadDdl(force = false) {
}
}
/** Loads the persisted table DDL when the dialog opens. */
/** Loads from the persisted snapshot by default; users can opt into a fresh database query on every open. */
watch(
() => props.open,
async (open) => {
@@ -140,7 +142,7 @@ watch(
const active = document.activeElement;
editorRootToRestoreFocus = active instanceof HTMLElement ? active.closest(".cm-editor") : null;
ddlContent.value = "";
await loadDdl();
await loadDdl(settingsStore.editorSettings.refreshDdlOnOpen);
},
{ immediate: true },
);
@@ -269,6 +271,11 @@ function retry() {
void loadDdl(true);
}
function setRefreshDdlOnOpen(value: boolean) {
settingsStore.updateEditorSettings({ refreshDdlOnOpen: value });
if (value) void loadDdl(true);
}
function onClose() {
emit("update:open", false);
}
@@ -298,6 +305,15 @@ function onClose() {
</div>
</div>
<DialogFooter>
<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">
<label for="ddl-refresh-on-open" class="cursor-pointer">{{ t("contextMenu.refreshDdlOnOpen") }}</label>
<HelpTooltip :label="t('contextMenu.refreshDdlOnOpenHint')" trigger-class="[&_svg]:h-3 [&_svg]:w-3">
{{ t("contextMenu.refreshDdlOnOpenHint") }}
</HelpTooltip>
</div>
</div>
<Button variant="outline" @click="onClose">{{ t("common.close") }}</Button>
<Button variant="outline" :disabled="ddlLoading" :title="t('structureEditor.refresh')" @click="loadDdl(true)">
<RefreshCw class="h-4 w-4" />
@@ -246,6 +246,13 @@ const tableConstraintsLoaded = ref(false);
const tableConstraintsForTab = computed(() => constraintsForConstraintsTab(tableConstraints.value, tableMetadataCapabilities.value.foreignKeys));
const tableInfoSearchQuery = ref("");
const tableInfoDdlPreRef = ref<HTMLPreElement | null>(null);
const activeTableInfoLoading = computed(() => {
if (tableInfoTab.value === "ddl") return tableDdlLoading.value;
if (tableInfoTab.value === "columns") return tableColumnsLoading.value;
if (tableInfoTab.value === "indexes") return tableIndexesLoading.value;
if (tableInfoTab.value === "foreignKeys") return tableForeignKeysLoading.value;
return tableInfoTab.value === "triggers" && tableTriggersLoading.value;
});
const SIDE_PANEL_MIN_WIDTH = 280;
const SIDE_PANEL_MAX_WIDTH = 900;
const sidePanelWidth = ref(settingsStore.editorSettings.tableInfoDrawerWidth || 420);
@@ -1098,7 +1105,7 @@ function tableMetadataRequest(row: ObjectBrowserRow): ObjectDdlRequest {
};
}
async function fetchTableDdl(force = false) {
async function fetchTableDdl(force = settingsStore.editorSettings.refreshDdlOnOpen) {
const row = sidePanelRow.value;
if (!row || (tableDdlLoaded.value && !force)) return;
const epoch = sidePanelGuard.capture();
@@ -3536,14 +3543,17 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<span class="block truncate">{{ tab.label }}</span>
</button>
</div>
<div class="px-2 py-1.5 border-b shrink-0 bg-background">
<div class="relative">
<div class="flex items-center gap-1 px-2 py-1.5 border-b shrink-0 bg-background">
<div class="relative min-w-0 flex-1">
<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
<input v-model="tableInfoSearchQuery" :placeholder="t('grid.tableInfoSearch')" class="w-full h-7 pl-7 pr-6 text-xs bg-muted/50 rounded border border-border focus:outline-none focus:border-primary/50" @keydown.escape="tableInfoSearchQuery = ''" />
<button v-if="tableInfoSearchQuery" class="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" @click="tableInfoSearchQuery = ''">
<X class="w-3 h-3" />
</button>
</div>
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" :disabled="activeTableInfoLoading" :title="t('structureEditor.refresh')" :aria-label="t('structureEditor.refresh')" @click="refreshActiveTableInfo">
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': activeTableInfoLoading }" />
</Button>
</div>
<div v-if="tableInfoTab === 'columns'" class="flex-1 min-h-0 overflow-auto">
<div v-if="tableColumnsLoading" class="h-full flex items-center justify-center">
@@ -0,0 +1,19 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../DdlViewDialog.vue", import.meta.url), "utf8");
describe("DdlViewDialog freshness", () => {
it("uses the persisted refresh preference when the dialog opens", () => {
expect(source).toMatch(/watch\(\s*\(\) => props\.open,[\s\S]*?await loadDdl\(settingsStore\.editorSettings\.refreshDdlOnOpen\);/);
});
it("refreshes the current DDL immediately when the preference is enabled", () => {
expect(source).toMatch(/function setRefreshDdlOnOpen\(value: boolean\)[\s\S]*?if \(value\) void loadDdl\(true\);/);
});
it("explains how the refresh preference affects cached DDL", () => {
expect(source).toContain('import { HelpTooltip } from "@/components/ui/tooltip";');
expect(source).toMatch(/<HelpTooltip :label="t\('contextMenu\.refreshDdlOnOpenHint'\)"[\s\S]*?t\("contextMenu\.refreshDdlOnOpenHint"\)[\s\S]*?<\/HelpTooltip>/);
});
});
@@ -47,6 +47,12 @@ describe("ObjectBrowser table metadata refresh", () => {
}
});
it("shares the DDL refresh preference and exposes an explicit refresh action", () => {
expect(source).toContain("async function fetchTableDdl(force = settingsStore.editorSettings.refreshDdlOnOpen)");
expect(source).toContain('@click="refreshActiveTableInfo"');
expect(source).not.toContain("setTableInfoRefreshDdlOnOpen");
});
it("keeps automatic object reloads free of extra metadata requests", () => {
expect(functionBody("reload")).not.toContain("refreshActiveTableInfo");
});