feat(ui): refresh latest ddl on demand
This commit is contained in:
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>/);
|
||||
});
|
||||
});
|
||||
+6
@@ -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");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user