fix(editor): keep edit-structure entry across table property tabs

* fix: 保留表属性编辑结构入口

* fix: 加深表属性页签选中态

---------

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg
2026-07-01 12:51:44 +08:00
committed by GitHub
co-authored by staff
parent 0c91fecf5d
commit 8129ecda88
6 changed files with 81 additions and 27 deletions
+16 -16
View File
@@ -6895,7 +6895,7 @@ function copyDdl() {
function openTableStructureEditor() {
if (!props.connectionId || !props.database || !props.tableMeta?.tableName || !canOpenTableStructureEditor.value) return;
queryStore.openTableStructure(props.connectionId, props.database, props.tableMeta.schema, props.tableMeta.tableName);
queryStore.openTableStructure(props.connectionId, props.database, props.tableMeta.schema, props.tableMeta.tableName, activeTableInfoTab.value);
}
function toggleDdlWrap() {
@@ -8656,18 +8656,18 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<div class="flex items-center gap-2 px-3 py-1.5 border-b shrink-0 bg-muted/20 h-9">
<TableProperties class="w-3.5 h-3.5 text-muted-foreground" />
<span class="text-xs font-medium flex-1 min-w-0 truncate">{{ tableMeta?.tableName }}</span>
<div v-if="activeTableInfoTab === 'ddl'" class="table-info-ddl-actions flex min-w-0 shrink-0 items-center gap-1">
<Button v-if="canOpenTableStructureEditor" variant="ghost" size="sm" class="table-info-ddl-action-button h-6 px-2 text-xs" :title="t('contextMenu.editStructure')" :aria-label="t('contextMenu.editStructure')" @click="openTableStructureEditor">
<PencilRuler class="w-3 h-3" />
<span class="table-info-ddl-action-label">{{ t("contextMenu.editStructure") }}</span>
</Button>
<Button variant="ghost" size="sm" class="table-info-ddl-action-button h-6 px-2 text-xs" :title="t('grid.copyDdl')" :aria-label="t('grid.copyDdl')" @click="copyDdl">
<div v-if="activeTableInfoTab === 'ddl'" class="table-info-actions flex min-w-0 shrink-0 items-center gap-1">
<Button variant="ghost" size="sm" class="table-info-action-button h-6 px-2 text-xs" :title="t('grid.copyDdl')" :aria-label="t('grid.copyDdl')" @click="copyDdl">
<Copy class="w-3 h-3" />
<span class="table-info-ddl-action-label">{{ t("grid.copyDdl") }}</span>
<span class="table-info-action-label">{{ t("grid.copyDdl") }}</span>
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6" :class="{ 'bg-accent': ddlWrap }" @click="toggleDdlWrap">
<WrapText class="w-3 h-3" />
</Button>
</div>
<Button v-if="activeTableInfoTab === 'ddl'" variant="ghost" size="icon" class="h-6 w-6" :class="{ 'bg-accent': ddlWrap }" @click="toggleDdlWrap">
<WrapText class="w-3 h-3" />
<Button v-if="canOpenTableStructureEditor" variant="ghost" size="sm" class="table-info-action-button h-6 px-2 text-xs" :title="t('contextMenu.editStructure')" :aria-label="t('contextMenu.editStructure')" @click="openTableStructureEditor">
<PencilRuler class="w-3 h-3" />
<span class="table-info-action-label">{{ t("contextMenu.editStructure") }}</span>
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="showTableInfo = false">
<X class="w-3 h-3" />
@@ -8677,8 +8677,8 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<button
v-for="tab in tableInfoTabs"
:key="tab.id"
class="h-9 min-w-0 px-1.5 text-[11px] text-muted-foreground border-b-2 border-transparent hover:bg-gray-200 dark:hover:bg-gray-800/50 hover:text-foreground"
:class="{ 'border-primary text-foreground bg-muted/40': activeTableInfoTab === tab.id }"
class="h-9 min-w-0 px-1.5 text-[11px] border-b-2 transition-colors"
:class="activeTableInfoTab === tab.id ? 'border-primary bg-gray-300/80 text-foreground dark:bg-gray-700/80' : 'border-transparent text-muted-foreground hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800/50'"
:title="tab.label"
@click="selectTableInfoTab(tab.id)"
>
@@ -9909,7 +9909,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
container-type: inline-size;
}
.table-info-ddl-action-button {
.table-info-action-button {
gap: 0.25rem;
max-width: 8rem;
overflow: hidden;
@@ -9918,7 +9918,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
padding-inline 180ms ease;
}
.table-info-ddl-action-label {
.table-info-action-label {
min-width: 0;
max-width: 6rem;
overflow: hidden;
@@ -9930,13 +9930,13 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
}
@container (max-width: 360px) {
.table-info-ddl-action-button {
.table-info-action-button {
width: 1.5rem;
max-width: 1.5rem;
padding-inline: 0;
}
.table-info-ddl-action-label {
.table-info-action-label {
max-width: 0;
opacity: 0;
}
@@ -1342,6 +1342,8 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
:database="activeTab.database"
:schema="activeTab.schema"
:table-name="activeTab.structureTableName || ''"
:initial-tab="activeTab.structureInitialTab"
:initial-tab-request-id="activeTab.structureInitialTabRequestId"
:draft="activeTab.structureDraft"
@update:draft="(draft) => (activeTab.structureDraft = draft)"
@saved="(commentChanged) => emit('structureEditorSaved', commentChanged)"
@@ -27,7 +27,7 @@ import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/li
import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities";
import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/tableStructureCapabilities";
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/jdbcDialect";
import type { TableStructureEditorDraft } from "@/types/database";
import type { TableInfoTab, TableStructureEditorDraft } from "@/types/database";
import {
buildStructureTargetLabel,
combineDataTypeForDatabase,
@@ -81,6 +81,8 @@ const props = defineProps<{
database: string;
schema?: string;
tableName: string;
initialTab?: TableInfoTab;
initialTabRequestId?: number;
draft?: TableStructureEditorDraft;
}>();
@@ -91,7 +93,7 @@ const emit = defineEmits<{
openSettings: [initialTab?: string, initialSection?: string];
}>();
const activeTab = ref("columns");
const activeTab = ref<TableInfoTab>("columns");
const loading = ref(false);
const saving = ref(false);
const postSaveRefreshing = ref(false);
@@ -1581,10 +1583,12 @@ function unregisterStructureEditorShortcuts() {
onMounted(() => {
resetState();
applyInitialStructureTab();
registerStructureEditorShortcuts();
void loadDynamicDataTypeOptions();
if (props.draft?.initialized) {
restoreDraft(props.draft);
applyInitialStructureTab();
} else if (isCreateMode.value) {
markDraftHydratedAndSync();
} else {
@@ -1616,14 +1620,27 @@ function firstStructureMetadataTab(capabilities = tableMetadataCapabilities.valu
return "columns";
}
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 resolveStructureMetadataTab(tab: TableInfoTab | undefined, capabilities = tableMetadataCapabilities.value): TableInfoTab {
if (tab && isStructureMetadataTabSupported(tab, capabilities)) return tab;
return firstStructureMetadataTab(capabilities);
}
function applyInitialStructureTab() {
if (!props.initialTab) return;
activeTab.value = resolveStructureMetadataTab(props.initialTab);
}
watch(tableMetadataCapabilities, (capabilities) => {
const supported =
(activeTab.value === "columns" && capabilities.columns) ||
(activeTab.value === "indexes" && capabilities.indexes) ||
(activeTab.value === "foreignKeys" && capabilities.foreignKeys) ||
(activeTab.value === "triggers" && capabilities.triggers) ||
(activeTab.value === "ddl" && capabilities.ddl && !isCreateMode.value);
if (!supported) activeTab.value = firstStructureMetadataTab(capabilities);
if (!isStructureMetadataTabSupported(activeTab.value, capabilities)) activeTab.value = firstStructureMetadataTab(capabilities);
});
watch([() => props.initialTab, () => props.initialTabRequestId], () => {
if (!props.initialTab) return;
applyInitialStructureTab();
});
watch([() => props.connectionId, () => props.database, databaseType], () => {
+11 -2
View File
@@ -2,7 +2,7 @@ import { defineStore } from "pinia";
import { uuid } from "@/lib/utils";
import { markRaw, ref, watch, computed } from "vue";
import { useI18n } from "vue-i18n";
import type { DatabaseType, QueryResult, QueryTab } from "@/types/database";
import type { DatabaseType, QueryResult, QueryTab, TableInfoTab } from "@/types/database";
import { orderPinnedFirst } from "@/lib/pinnedItems";
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
@@ -745,11 +745,18 @@ export const useQueryStore = defineStore("query", () => {
return id;
}
function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string) {
function applyTableStructureInitialTab(tab: QueryTab, initialTab?: TableInfoTab) {
if (!initialTab) return;
tab.structureInitialTab = initialTab;
tab.structureInitialTabRequestId = (tab.structureInitialTabRequestId ?? 0) + 1;
}
function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string, initialTab?: TableInfoTab) {
const resolvedTableName = tableName || "";
if (resolvedTableName) {
const existing = tabs.value.find((tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.structureTableName || "") === resolvedTableName);
if (existing) {
applyTableStructureInitialTab(existing, initialTab);
activeTabId.value = existing.id;
return existing.id;
}
@@ -769,6 +776,8 @@ export const useQueryStore = defineStore("query", () => {
isExplaining: false,
mode: "structure",
structureTableName: resolvedTableName,
structureInitialTab: initialTab,
structureInitialTabRequestId: initialTab ? 1 : undefined,
};
tabs.value.push(tab);
activeTabId.value = id;
+2
View File
@@ -643,6 +643,8 @@ export interface QueryTab {
nacosNamespace?: string;
nacosNamespaceName?: string;
structureTableName?: string;
structureInitialTab?: TableInfoTab;
structureInitialTabRequestId?: number;
structureDraft?: TableStructureEditorDraft;
objectBrowser?: {
schema?: string;
+24
View File
@@ -2759,6 +2759,30 @@ test("new table structure tabs can open multiple drafts while existing tables st
}
});
test("reopening table structure tabs records the requested initial tab", () => {
const restoreStorage = installMemoryStorage();
try {
setActivePinia(createPinia());
const store = useQueryStore();
const structureId = store.openTableStructure("conn-1", "db", "public", "users", "indexes");
const firstTab = store.tabs.find((item) => item.id === structureId);
assert.equal(firstTab?.structureInitialTab, "indexes");
assert.equal(firstTab?.structureInitialTabRequestId, 1);
const reusedStructureId = store.openTableStructure("conn-1", "db", "public", "users", "foreignKeys");
const reusedTab = store.tabs.find((item) => item.id === reusedStructureId);
assert.equal(reusedStructureId, structureId);
assert.equal(reusedTab?.structureInitialTab, "foreignKeys");
assert.equal(reusedTab?.structureInitialTabRequestId, 2);
assert.equal(store.activeTabId, structureId);
} finally {
restoreStorage();
}
});
test("table structure refresh versions are scoped by table target", () => {
setActivePinia(createPinia());
const store = useQueryStore();