fix(structure): default edit view to DDL
This commit is contained in:
parent
34cf69132d
commit
2f3d1de46a
|
|
@ -7677,8 +7677,21 @@ function clampCellDetailPanelSize(value: number, layout = cellDetailPanelLayout.
|
|||
// Table info drawers are tied to a single grid instance. Keeping this state
|
||||
// module-global leaks the drawer into other kept-alive tabs.
|
||||
const showTableInfo = ref(false);
|
||||
const activeTableInfoTab = ref<TableInfoTab>("columns");
|
||||
const activeTableInfoTab = ref<TableInfoTab>("ddl");
|
||||
const ddlContent = ref("");
|
||||
const ddlPreRef = ref<HTMLPreElement | null>(null);
|
||||
function onDdlKeydown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
||||
e.preventDefault();
|
||||
const el = ddlPreRef.value;
|
||||
if (!el) return;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
}
|
||||
const ddlLoading = ref(false);
|
||||
const ddlWidth = ref(settingsStore.editorSettings.tableInfoDrawerWidth);
|
||||
const detailPanelHeight = ref(settingsStore.editorSettings.cellDetailDrawerWidth);
|
||||
|
|
@ -7717,6 +7730,14 @@ watch(activeTableInfoTab, () => {
|
|||
searchQuery.value = "";
|
||||
});
|
||||
|
||||
watch([activeTableInfoTab, ddlLoading], ([tab, loading]) => {
|
||||
if (tab === "ddl" && !loading) {
|
||||
void nextTick(() => {
|
||||
ddlPreRef.value?.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => settingsStore.editorSettings.tableInfoDrawerWidth,
|
||||
(width) => {
|
||||
|
|
@ -7779,6 +7800,9 @@ const mongoConnectionConfig = computed(() => connectionStore.getConfig(props.con
|
|||
const canManageMongoIndexes = computed(() => resolvedDatabaseType.value === "mongodb" && !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && mongoConnectionConfig.value?.db_type === "mongodb" && mongoConnectionConfig.value?.driver_profile !== "mongodb-legacy");
|
||||
const tableInfoTabs = computed(() => {
|
||||
const tabs: TableInfoTabItem[] = [];
|
||||
if (tableMetadataCapabilities.value.ddl) {
|
||||
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
|
||||
}
|
||||
if (tableMetadataCapabilities.value.columns) {
|
||||
tabs.push({
|
||||
id: "columns",
|
||||
|
|
@ -7801,9 +7825,6 @@ const tableInfoTabs = computed(() => {
|
|||
if (tableMetadataCapabilities.value.triggers) {
|
||||
tabs.push({ id: "triggers", label: t("grid.tableInfoTriggers"), icon: RotateCcw, count: triggers.value.length });
|
||||
}
|
||||
if (tableMetadataCapabilities.value.ddl) {
|
||||
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
|
||||
}
|
||||
return tabs;
|
||||
});
|
||||
const tableInfoTabListStyle = computed(() => ({
|
||||
|
|
@ -10168,7 +10189,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<pre v-else-if="activeTableInfoTab === 'ddl' && !ddlLoading" data-native-clipboard class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text" :class="ddlWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'" v-html="filteredDdlContent"></pre>
|
||||
<pre
|
||||
v-else-if="activeTableInfoTab === 'ddl' && !ddlLoading"
|
||||
ref="ddlPreRef"
|
||||
data-native-clipboard
|
||||
tabindex="0"
|
||||
class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text outline-none"
|
||||
:class="ddlWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'"
|
||||
v-html="filteredDdlContent"
|
||||
@keydown="onDdlKeydown"
|
||||
></pre>
|
||||
<div v-else class="flex-1 flex items-center justify-center">
|
||||
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
|
|||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
import { type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql";
|
||||
import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates";
|
||||
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { getTableMetadataCapabilities, firstStructureMetadataTab, isStructureMetadataTabSupported } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import type { TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget, TableStructureEditorViewport } from "@/types/database";
|
||||
|
|
@ -107,7 +107,7 @@ const emit = defineEmits<{
|
|||
openSettings: [initialTab?: string, initialSection?: string];
|
||||
}>();
|
||||
|
||||
const activeTab = ref<TableInfoTab>("columns");
|
||||
const activeTab = ref<TableInfoTab>("ddl");
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const postSaveRefreshing = ref(false);
|
||||
|
|
@ -117,6 +117,19 @@ const foreignKeysLoading = ref(false);
|
|||
const triggersLoading = ref(false);
|
||||
const ddlContent = ref("");
|
||||
const ddlLoading = ref(false);
|
||||
const ddlPreRef = ref<HTMLPreElement | null>(null);
|
||||
function onDdlKeydown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
||||
e.preventDefault();
|
||||
const el = ddlPreRef.value;
|
||||
if (!el) return;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
}
|
||||
const ddlFetched = ref(false);
|
||||
|
||||
async function fetchDdl() {
|
||||
|
|
@ -1095,7 +1108,6 @@ function clearDraft() {
|
|||
}
|
||||
|
||||
function resetState() {
|
||||
activeTab.value = "columns";
|
||||
loading.value = false;
|
||||
saving.value = false;
|
||||
postSaveRefreshing.value = false;
|
||||
|
|
@ -2075,7 +2087,8 @@ onMounted(() => {
|
|||
void loadDynamicDataTypeOptions();
|
||||
if (props.draft?.initialized) {
|
||||
restoreDraft(props.draft);
|
||||
applyInitialStructureTab();
|
||||
// A restored draft owns its saved tab unless navigation explicitly requested another one.
|
||||
applyInitialStructureTab(false);
|
||||
applyInitialStructureTarget();
|
||||
void hydrateRestoredDraftFromDatabase().then(() => applyInitialStructureTarget());
|
||||
} else if (isCreateMode.value) {
|
||||
|
|
@ -2106,27 +2119,25 @@ onBeforeUnmount(() => {
|
|||
persistStructureDensity();
|
||||
});
|
||||
|
||||
function firstStructureMetadataTab(capabilities = tableMetadataCapabilities.value) {
|
||||
if (capabilities.columns) return "columns";
|
||||
if (capabilities.indexes) return "indexes";
|
||||
if (capabilities.foreignKeys) return "foreignKeys";
|
||||
if (capabilities.triggers) return "triggers";
|
||||
if (capabilities.ddl && !isCreateMode.value) return "ddl";
|
||||
return "columns";
|
||||
function localFirstStructureMetadataTab(capabilities = tableMetadataCapabilities.value) {
|
||||
return firstStructureMetadataTab(capabilities, isCreateMode.value);
|
||||
}
|
||||
|
||||
function isStructureMetadataTabSupported(tab: TableInfoTab, capabilities = tableMetadataCapabilities.value) {
|
||||
return (tab === "columns" && capabilities.columns) || (tab === "indexes" && capabilities.indexes) || (tab === "foreignKeys" && capabilities.foreignKeys) || (tab === "triggers" && capabilities.triggers) || (tab === "ddl" && capabilities.ddl && !isCreateMode.value);
|
||||
function localIsStructureMetadataTabSupported(tab: TableInfoTab, capabilities = tableMetadataCapabilities.value) {
|
||||
return isStructureMetadataTabSupported(tab, capabilities, isCreateMode.value);
|
||||
}
|
||||
|
||||
function resolveStructureMetadataTab(tab: TableInfoTab | undefined, capabilities = tableMetadataCapabilities.value): TableInfoTab {
|
||||
if (tab && isStructureMetadataTabSupported(tab, capabilities)) return tab;
|
||||
return firstStructureMetadataTab(capabilities);
|
||||
if (tab && localIsStructureMetadataTabSupported(tab, capabilities)) return tab;
|
||||
return localFirstStructureMetadataTab(capabilities);
|
||||
}
|
||||
|
||||
function applyInitialStructureTab() {
|
||||
if (!props.initialTab) return;
|
||||
activeTab.value = resolveStructureMetadataTab(props.initialTab);
|
||||
function applyInitialStructureTab(useDefault = true) {
|
||||
if (props.initialTab) {
|
||||
activeTab.value = resolveStructureMetadataTab(props.initialTab);
|
||||
} else if (useDefault) {
|
||||
activeTab.value = resolveStructureMetadataTab(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function initialTargetKey(target: TableStructureEditorTarget): string {
|
||||
|
|
@ -2166,7 +2177,7 @@ function applyInitialStructureTarget() {
|
|||
}
|
||||
|
||||
watch(tableMetadataCapabilities, (capabilities) => {
|
||||
if (!isStructureMetadataTabSupported(activeTab.value, capabilities)) activeTab.value = firstStructureMetadataTab(capabilities);
|
||||
if (!localIsStructureMetadataTabSupported(activeTab.value, capabilities)) activeTab.value = localFirstStructureMetadataTab(capabilities);
|
||||
});
|
||||
|
||||
watch([() => props.initialTab, () => props.initialTabRequestId, () => props.initialTarget], () => {
|
||||
|
|
@ -2218,9 +2229,21 @@ watch(refreshVersion, (version, previous) => {
|
|||
void loadStructure(true);
|
||||
});
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === "ddl") {
|
||||
void fetchDdl();
|
||||
watch(
|
||||
activeTab,
|
||||
(tab) => {
|
||||
if (tab === "ddl") {
|
||||
void fetchDdl();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
if (tab === "ddl" && !loading) {
|
||||
void nextTick(() => {
|
||||
ddlPreRef.value?.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -2263,11 +2286,11 @@ watch(activeTab, (tab) => {
|
|||
<Tabs v-model="activeTab" class="flex h-full min-h-0 flex-col">
|
||||
<div class="flex shrink-0 items-center justify-between gap-2 border-b px-2 py-[var(--structure-header-py)]">
|
||||
<TabsList>
|
||||
<TabsTrigger v-if="tableMetadataCapabilities.ddl && !isCreateMode" value="ddl">DDL</TabsTrigger>
|
||||
<TabsTrigger v-if="tableMetadataCapabilities.columns" value="columns">{{ t("structureEditor.columns") }}</TabsTrigger>
|
||||
<TabsTrigger v-if="tableMetadataCapabilities.indexes" value="indexes">{{ t("structureEditor.indexes") }}</TabsTrigger>
|
||||
<TabsTrigger v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys">{{ t("structureEditor.foreignKeys") }}</TabsTrigger>
|
||||
<TabsTrigger v-if="tableMetadataCapabilities.triggers" value="triggers">{{ t("structureEditor.triggers") }}</TabsTrigger>
|
||||
<TabsTrigger v-if="tableMetadataCapabilities.ddl && !isCreateMode" value="ddl">DDL</TabsTrigger>
|
||||
</TabsList>
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<div class="flex items-center gap-1.5">
|
||||
|
|
@ -2896,7 +2919,7 @@ watch(activeTab, (tab) => {
|
|||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
<pre v-else class="m-0 min-h-0 flex-1 whitespace-pre p-3 font-mono text-xs leading-5 select-text" v-html="ddlContent ? (sqlHighlighter?.(ddlContent) ?? ddlContent) : t('structureEditor.emptyReadonly')"></pre>
|
||||
<pre v-else ref="ddlPreRef" tabindex="0" class="m-0 min-h-0 flex-1 whitespace-pre p-3 font-mono text-xs leading-5 select-text outline-none" v-html="ddlContent ? (sqlHighlighter?.(ddlContent) ?? ddlContent) : t('structureEditor.emptyReadonly')" @keydown="onDdlKeydown"></pre>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { DatabaseType } from "@/types/database";
|
||||
import type { DatabaseType, TableInfoTab } from "@/types/database";
|
||||
|
||||
export interface TableMetadataCapabilities {
|
||||
columns: boolean;
|
||||
|
|
@ -71,3 +71,16 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
|
|||
export function getTableMetadataCapabilities(dbType?: DatabaseType): TableMetadataCapabilities {
|
||||
return { ...defaultCapabilities, ...(dbType ? capabilityByType[dbType] : undefined) };
|
||||
}
|
||||
|
||||
export function firstStructureMetadataTab(capabilities: TableMetadataCapabilities, isCreateMode: boolean): TableInfoTab {
|
||||
if (!isCreateMode && capabilities.ddl) return "ddl";
|
||||
if (capabilities.columns) return "columns";
|
||||
if (capabilities.indexes) return "indexes";
|
||||
if (capabilities.foreignKeys) return "foreignKeys";
|
||||
if (capabilities.triggers) return "triggers";
|
||||
return "columns";
|
||||
}
|
||||
|
||||
export function isStructureMetadataTabSupported(tab: TableInfoTab, capabilities: TableMetadataCapabilities, isCreateMode: boolean): boolean {
|
||||
return (tab === "columns" && capabilities.columns) || (tab === "indexes" && capabilities.indexes) || (tab === "foreignKeys" && capabilities.foreignKeys) || (tab === "triggers" && capabilities.triggers) || (tab === "ddl" && capabilities.ddl && !isCreateMode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
applyManticoreDdlColumnExtras,
|
||||
|
|
@ -21,7 +22,8 @@ import {
|
|||
rehydrateColumnDraftsFromMetadata,
|
||||
toColumnNames,
|
||||
} from "../../apps/desktop/src/lib/table/tableStructureEditorState.ts";
|
||||
import type { ColumnInfo, IndexInfo } from "../../apps/desktop/src/types/database.ts";
|
||||
import { firstStructureMetadataTab, isStructureMetadataTabSupported } from "../../apps/desktop/src/lib/table/tableMetadataCapabilities.ts";
|
||||
import type { ColumnInfo, IndexInfo, TableInfoTab } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
const columns: ColumnInfo[] = [
|
||||
{
|
||||
|
|
@ -432,3 +434,43 @@ test("allows Manticore Search column properties only before the column exists",
|
|||
assert.equal(canEditManticoreColumnProperties("manticoresearch", true), false);
|
||||
assert.equal(canEditManticoreColumnProperties("mysql", false), false);
|
||||
});
|
||||
|
||||
const fullCapabilities = { columns: true, indexes: true, foreignKeys: true, triggers: true, ddl: true };
|
||||
const noDdlCapabilities = { columns: true, indexes: true, foreignKeys: true, triggers: true, ddl: false };
|
||||
|
||||
test("defaults to DDL tab for edit mode with full capabilities", () => {
|
||||
assert.equal(firstStructureMetadataTab(fullCapabilities, false), "ddl");
|
||||
});
|
||||
|
||||
test("defaults to columns tab for create mode", () => {
|
||||
assert.equal(firstStructureMetadataTab(fullCapabilities, true), "columns");
|
||||
});
|
||||
|
||||
test("falls back to columns tab when DDL is not available in edit mode", () => {
|
||||
assert.equal(firstStructureMetadataTab(noDdlCapabilities, false), "columns");
|
||||
});
|
||||
|
||||
test("preserves a restored structure draft tab without an explicit initial tab", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/structure/TableStructureEditor.vue", "utf8");
|
||||
const restoredDraftBlock = source.match(/if \(props\.draft\?\.initialized\) \{[\s\S]*?\n \} else if/);
|
||||
|
||||
assert.ok(restoredDraftBlock);
|
||||
assert.match(restoredDraftBlock[0], /restoreDraft\(props\.draft\);[\s\S]*applyInitialStructureTab\(false\);/);
|
||||
});
|
||||
|
||||
test("supports DDL tab in edit mode", () => {
|
||||
assert.equal(isStructureMetadataTabSupported("ddl", fullCapabilities, false), true);
|
||||
});
|
||||
|
||||
test("does not support DDL tab in create mode", () => {
|
||||
assert.equal(isStructureMetadataTabSupported("ddl", fullCapabilities, true), false);
|
||||
});
|
||||
|
||||
test("does not support DDL tab when capability is disabled", () => {
|
||||
assert.equal(isStructureMetadataTabSupported("ddl", noDdlCapabilities, false), false);
|
||||
});
|
||||
|
||||
test("supports columns tab in both modes", () => {
|
||||
assert.equal(isStructureMetadataTabSupported("columns", fullCapabilities, false), true);
|
||||
assert.equal(isStructureMetadataTabSupported("columns", fullCapabilities, true), true);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue