feat(structure): open editor from field and index nodes

This commit is contained in:
t8y2 committed 2026-07-06 14:16:43 +08:00
1 parent 564c5beeff
commit 92ef1ac3cf
13 files changed
+258 -25

No files matched your search

@@ -1412,6 +1412,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
:table-name="activeTab.structureTableName || ''" :table-name="activeTab.structureTableName || ''"
:initial-tab="activeTab.structureInitialTab" :initial-tab="activeTab.structureInitialTab"
:initial-tab-request-id="activeTab.structureInitialTabRequestId" :initial-tab-request-id="activeTab.structureInitialTabRequestId"
:initial-target="activeTab.structureInitialTarget"
:draft="activeTab.structureDraft" :draft="activeTab.structureDraft"
@update:draft="(draft) => (activeTab.structureDraft = draft)" @update:draft="(draft) => (activeTab.structureDraft = draft)"
@saved="(commentChanged) => emit('structureEditorSaved', commentChanged)" @saved="(commentChanged) => emit('structureEditorSaved', commentChanged)"
@@ -3790,8 +3790,22 @@ function openTableImport() {
function openStructureEditor() { function openStructureEditor() {
const node = props.node; const node = props.node;
if (node.type !== "table" || !node.connectionId || !node.database) return; if (!node.connectionId || !node.database) return;
queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.label); if (node.type === "table") {
queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.label);
return;
}
if (node.type === "column" && node.tableName) {
const columnName = tableChildDropObjectName(node).trim();
if (!columnName) return;
queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.tableName, "columns", { kind: "column", name: columnName });
return;
}
if (node.type === "index" && node.tableName) {
const indexName = tableChildDropObjectName(node).trim();
if (!indexName) return;
queryStore.openTableStructure(node.connectionId, node.database, node.schema, node.tableName, "indexes", { kind: "index", name: indexName });
}
} }
function openFieldLineage() { function openFieldLineage() {
@@ -3835,7 +3849,8 @@ const canOpenTableImport = computed(() => {
return props.node.type === "table" && !isSqlServerLinkedNode(props.node) && !!props.node.database && supportsTableImport(currentDatabaseType()); return props.node.type === "table" && !isSqlServerLinkedNode(props.node) && !!props.node.database && supportsTableImport(currentDatabaseType());
}); });
const canOpenStructureEditor = computed(() => { const canOpenStructureEditor = computed(() => {
return props.node.type === "table" && !isSqlServerLinkedNode(props.node) && !!props.node.database && supportsTableStructureEditing(currentTableStructureDatabaseType()); const editableNode = props.node.type === "table" || ((props.node.type === "column" || props.node.type === "index") && !!props.node.tableName);
return editableNode && !isSqlServerLinkedNode(props.node) && !!props.node.connectionId && !!props.node.database && supportsTableStructureEditing(currentTableStructureDatabaseType());
}); });
const canOpenFieldLineage = computed(() => { const canOpenFieldLineage = computed(() => {
return props.node.type === "column" && !!props.node.database && !!props.node.tableName && supportsFieldLineage(currentDatabaseType()); return props.node.type === "column" && !!props.node.database && !!props.node.tableName && supportsFieldLineage(currentDatabaseType());
@@ -4756,9 +4771,16 @@ function treeItemMenuItems(): ContextMenuItem[] {
// 7. Column // 7. Column
if (node.type === "column") { if (node.type === "column") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
const columnActions: ContextMenuItem[] = [];
if (canOpenStructureEditor.value) {
columnActions.push({ label: t("contextMenu.editColumn"), action: openStructureEditor, icon: PencilRuler });
}
if (canOpenFieldLineage.value) { if (canOpenFieldLineage.value) {
columnActions.push({ label: t("lineage.open"), action: openFieldLineage, icon: Network });
}
if (columnActions.length > 0) {
items.push({ label: "", separator: true }); items.push({ label: "", separator: true });
items.push({ label: t("lineage.open"), action: openFieldLineage, icon: Network }); items.push(...columnActions);
} }
if (canDropTableChildObject.value) { if (canDropTableChildObject.value) {
items.push({ label: "", separator: true }); items.push({ label: "", separator: true });
@@ -4775,6 +4797,10 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (node.type === "index" || node.type === "fkey" || node.type === "trigger") { if (node.type === "index" || node.type === "fkey" || node.type === "trigger") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
if (node.type === "index" && canOpenStructureEditor.value) {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.editIndex"), action: openStructureEditor, icon: PencilRuler });
}
if (node.type === "index" && canDropMongoIndex.value) { if (node.type === "index" && canDropMongoIndex.value) {
items.push({ label: "", separator: true }); items.push({ label: "", separator: true });
items.push({ items.push({
@@ -28,7 +28,7 @@ import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/li
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities"; import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities"; import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import type { TableInfoTab, TableStructureEditorDraft } from "@/types/database"; import type { TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget } from "@/types/database";
import { import {
buildStructureTargetLabel, buildStructureTargetLabel,
combineDataTypeForDatabase, combineDataTypeForDatabase,
@@ -85,6 +85,7 @@ const props = defineProps<{
tableName: string; tableName: string;
initialTab?: TableInfoTab; initialTab?: TableInfoTab;
initialTabRequestId?: number; initialTabRequestId?: number;
initialTarget?: TableStructureEditorTarget;
draft?: TableStructureEditorDraft; draft?: TableStructureEditorDraft;
}>(); }>();
@@ -494,8 +495,12 @@ const resizing = ref<{ col: number; startX: number; startW: number } | null>(nul
const columnSearchInputRef = ref<InstanceType<typeof Input>>(); const columnSearchInputRef = ref<InstanceType<typeof Input>>();
const columnSearchText = ref(""); const columnSearchText = ref("");
const highlightedColumnId = ref<string | null>(null); const highlightedColumnId = ref<string | null>(null);
const indexSearchInputRef = ref<InstanceType<typeof Input>>();
const indexSearchText = ref("");
const highlightedIndexId = ref<string | null>(null);
const sqlPreviewCollapsed = ref(loadSqlPreviewCollapsed()); const sqlPreviewCollapsed = ref(loadSqlPreviewCollapsed());
let columnHighlightTimer: ReturnType<typeof window.setTimeout> | undefined; let columnHighlightTimer: ReturnType<typeof window.setTimeout> | undefined;
let indexHighlightTimer: ReturnType<typeof window.setTimeout> | undefined;
watch( watch(
structureDensity, structureDensity,
@@ -689,6 +694,12 @@ const filteredColumnRowIds = computed(() => {
); );
}); });
const columnSearchMatchCount = computed(() => (columnSearchText.value.trim() ? filteredColumnRowIds.value.size : 0)); const columnSearchMatchCount = computed(() => (columnSearchText.value.trim() ? filteredColumnRowIds.value.size : 0));
const filteredIndexRowIds = computed(() => {
const query = indexSearchText.value.trim().toLowerCase();
if (!query) return new Set<string>();
return new Set(indexes.value.filter((index) => indexMatchesSearch(index, query)).map((index) => index.id));
});
const indexSearchMatchCount = computed(() => (indexSearchText.value.trim() ? filteredIndexRowIds.value.size : 0));
const foreignKeyActionOptions = ["", "CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]; const foreignKeyActionOptions = ["", "CASCADE", "SET NULL", "RESTRICT", "NO ACTION"];
const triggerTimingOptions = ["BEFORE", "AFTER"]; const triggerTimingOptions = ["BEFORE", "AFTER"];
const triggerEventOptions = ["INSERT", "UPDATE", "DELETE"]; const triggerEventOptions = ["INSERT", "UPDATE", "DELETE"];
@@ -723,6 +734,10 @@ let restoringDraft = false;
let syncingDraft = false; let syncingDraft = false;
let draftHydrated = false; let draftHydrated = false;
let hydratingRestoredDraft = false; let hydratingRestoredDraft = false;
// A context-menu target may arrive before metadata rows render, so search text
// and row scrolling are tracked separately for each request.
let appliedInitialTargetSearchKey = "";
let appliedInitialTargetScrollKey = "";
function cloneDraftValue<T>(value: T): T { function cloneDraftValue<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T; return JSON.parse(JSON.stringify(value)) as T;
@@ -977,6 +992,12 @@ function resetState() {
newTableName.value = ""; newTableName.value = "";
tableComment.value = ""; tableComment.value = "";
originalTableComment.value = ""; originalTableComment.value = "";
columnSearchText.value = "";
highlightedColumnId.value = null;
indexSearchText.value = "";
highlightedIndexId.value = null;
appliedInitialTargetSearchKey = "";
appliedInitialTargetScrollKey = "";
} }
async function reloadStructureFromDatabase() { async function reloadStructureFromDatabase() {
@@ -1441,6 +1462,76 @@ function onColumnSearchKeydown(event: KeyboardEvent) {
scrollToColumnSearchMatch(event.shiftKey ? -1 : 1); scrollToColumnSearchMatch(event.shiftKey ? -1 : 1);
} }
function indexMatchesSearch(index: EditableStructureIndex, searchQuery = indexSearchText.value.trim().toLowerCase()): boolean {
if (!searchQuery) return false;
return [index.name, toColumnNames(index.columns), index.includedColumns.join(", "), index.indexType, index.filter, index.comment].some((value) =>
String(value ?? "")
.toLowerCase()
.includes(searchQuery),
);
}
function indexFieldMatchesSearch(value: string | null | undefined): boolean {
const query = indexSearchText.value.trim().toLowerCase();
return (
!!query &&
String(value ?? "")
.toLowerCase()
.includes(query)
);
}
function indexRowClass(index: EditableStructureIndex) {
const isSearchMatch = filteredIndexRowIds.value.has(index.id);
return {
"bg-destructive/5 opacity-60": index.markedForDrop,
"structure-column-search-match": isSearchMatch,
"structure-column-search-current": highlightedIndexId.value === index.id,
};
}
function indexSearchFieldClass(index: EditableStructureIndex, value: string | null | undefined) {
const matches = indexFieldMatchesSearch(value);
return {
"!border-primary/60 !bg-primary/10": matches,
"!border-primary !ring-2 !ring-primary/30": matches && highlightedIndexId.value === index.id,
};
}
function focusIndexSearch() {
activeTab.value = "indexes";
void nextTick(() => {
const input = indexSearchInputRef.value?.$el as HTMLInputElement | undefined;
input?.focus();
input?.select();
});
}
function scrollToIndexSearchMatch(direction: 1 | -1 = 1) {
const query = indexSearchText.value.trim();
if (!query) {
focusIndexSearch();
return;
}
const rows = Array.from(rootRef.value?.querySelectorAll<HTMLElement>("[data-index-row-index]") ?? []);
const matches = indexes.value.map((index, rowIndex) => ({ index, rowIndex })).filter(({ index }) => indexMatchesSearch(index));
if (!matches.length) return;
const currentIndex = highlightedIndexId.value ? matches.findIndex(({ index }) => index.id === highlightedIndexId.value) : -1;
const nextMatch = matches[(currentIndex + direction + matches.length) % matches.length] ?? matches[0];
highlightedIndexId.value = nextMatch.index.id;
rows[nextMatch.rowIndex]?.scrollIntoView({ block: "center", inline: "nearest" });
if (indexHighlightTimer) window.clearTimeout(indexHighlightTimer);
indexHighlightTimer = window.setTimeout(() => {
highlightedIndexId.value = null;
}, 1800);
}
function onIndexSearchKeydown(event: KeyboardEvent) {
if (event.key !== "Enter") return;
event.preventDefault();
scrollToIndexSearchMatch(event.shiftKey ? -1 : 1);
}
function columnDragInsertionIndex(index: number, event: DragEvent): number { function columnDragInsertionIndex(index: number, event: DragEvent): number {
const target = event.currentTarget; const target = event.currentTarget;
if (!(target instanceof HTMLElement)) return index; if (!(target instanceof HTMLElement)) return index;
@@ -1816,16 +1907,18 @@ function unregisterStructureEditorShortcuts() {
onMounted(() => { onMounted(() => {
resetState(); resetState();
applyInitialStructureTab(); applyInitialStructureTab();
applyInitialStructureTarget();
registerStructureEditorShortcuts(); registerStructureEditorShortcuts();
void loadDynamicDataTypeOptions(); void loadDynamicDataTypeOptions();
if (props.draft?.initialized) { if (props.draft?.initialized) {
restoreDraft(props.draft); restoreDraft(props.draft);
applyInitialStructureTab(); applyInitialStructureTab();
void hydrateRestoredDraftFromDatabase(); applyInitialStructureTarget();
void hydrateRestoredDraftFromDatabase().then(() => applyInitialStructureTarget());
} else if (isCreateMode.value) { } else if (isCreateMode.value) {
markDraftHydratedAndSync(); markDraftHydratedAndSync();
} else { } else {
void loadStructure(false, FULL_STRUCTURE_REFRESH_SCOPE, true, { blockSecondaryMetadata: true }); void loadStructure(false, FULL_STRUCTURE_REFRESH_SCOPE, true, { blockSecondaryMetadata: true }).then(() => applyInitialStructureTarget());
} }
}); });
@@ -1834,7 +1927,8 @@ onActivated(() => {
void loadDynamicDataTypeOptions(); void loadDynamicDataTypeOptions();
if (props.draft?.initialized && !draftHydrated) { if (props.draft?.initialized && !draftHydrated) {
restoreDraft(props.draft); restoreDraft(props.draft);
void hydrateRestoredDraftFromDatabase(); applyInitialStructureTarget();
void hydrateRestoredDraftFromDatabase().then(() => applyInitialStructureTarget());
} }
}); });
onDeactivated(unregisterStructureEditorShortcuts); onDeactivated(unregisterStructureEditorShortcuts);
@@ -1843,6 +1937,7 @@ onBeforeUnmount(() => {
unregisterStructureEditorShortcuts(); unregisterStructureEditorShortcuts();
clearSqlPreviewState(); clearSqlPreviewState();
if (columnHighlightTimer) window.clearTimeout(columnHighlightTimer); if (columnHighlightTimer) window.clearTimeout(columnHighlightTimer);
if (indexHighlightTimer) window.clearTimeout(indexHighlightTimer);
persistStructureDensity(); persistStructureDensity();
}); });
@@ -1869,13 +1964,53 @@ function applyInitialStructureTab() {
activeTab.value = resolveStructureMetadataTab(props.initialTab); activeTab.value = resolveStructureMetadataTab(props.initialTab);
} }
function initialTargetKey(target: TableStructureEditorTarget): string {
return `${props.initialTabRequestId ?? 0}:${target.kind}:${target.name}`;
}
function applyInitialStructureTarget() {
const target = props.initialTarget;
const targetName = target?.name.trim();
if (!target || !targetName) return;
const key = initialTargetKey(target);
if (appliedInitialTargetSearchKey !== key) {
if (target.kind === "column") {
activeTab.value = resolveStructureMetadataTab("columns");
columnSearchText.value = targetName;
highlightedColumnId.value = null;
} else {
activeTab.value = resolveStructureMetadataTab("indexes");
indexSearchText.value = targetName;
highlightedIndexId.value = null;
}
appliedInitialTargetSearchKey = key;
}
if (appliedInitialTargetScrollKey === key) return;
const hasMatch = target.kind === "column" ? columns.value.some((column) => columnMatchesSearch(column)) : indexes.value.some((index) => indexMatchesSearch(index));
if (!hasMatch) return;
appliedInitialTargetScrollKey = key;
void nextTick(() => {
if (target.kind === "column") {
scrollToColumnSearchMatch(1);
} else {
scrollToIndexSearchMatch(1);
}
});
}
watch(tableMetadataCapabilities, (capabilities) => { watch(tableMetadataCapabilities, (capabilities) => {
if (!isStructureMetadataTabSupported(activeTab.value, capabilities)) activeTab.value = firstStructureMetadataTab(capabilities); if (!isStructureMetadataTabSupported(activeTab.value, capabilities)) activeTab.value = firstStructureMetadataTab(capabilities);
}); });
watch([() => props.initialTab, () => props.initialTabRequestId], () => { watch([() => props.initialTab, () => props.initialTabRequestId, () => props.initialTarget], () => {
if (!props.initialTab) return; if (props.initialTab) applyInitialStructureTab();
applyInitialStructureTab(); applyInitialStructureTarget();
});
watch([columns, indexes], () => {
applyInitialStructureTarget();
}); });
watch([() => props.connectionId, () => props.database, databaseType], () => { watch([() => props.connectionId, () => props.database, databaseType], () => {
@@ -1893,6 +2028,7 @@ watch(
watch(activeTab, () => { watch(activeTab, () => {
highlightedColumnId.value = null; highlightedColumnId.value = null;
highlightedIndexId.value = null;
syncDraftToParent(); syncDraftToParent();
}); });
@@ -2036,6 +2172,19 @@ watch(activeTab, (tab) => {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{{ t("structureEditor.configureColumnTemplates") }}</TooltipContent> <TooltipContent>{{ t("structureEditor.configureColumnTemplates") }}</TooltipContent>
</Tooltip> </Tooltip>
<div v-if="activeTab === 'indexes'" class="relative flex w-40 shrink-0 items-center">
<Search :class="[structureIconClass, 'pointer-events-none absolute left-2 text-muted-foreground']" />
<Input ref="indexSearchInputRef" v-model="indexSearchText" :placeholder="t('structureEditor.searchIndexes')" :class="[structureControlClass, 'pl-7 pr-14 text-[length:var(--structure-font-size)] placeholder:text-[length:var(--structure-font-size)]']" @keydown="onIndexSearchKeydown" />
<button
v-if="indexSearchText"
type="button"
class="absolute right-1.5 top-1/2 -translate-y-1/2 rounded px-1 text-[length:var(--structure-font-size)] text-muted-foreground hover:bg-muted hover:text-foreground"
:title="t('structureEditor.nextIndexMatch')"
@click="scrollToIndexSearchMatch(1)"
>
{{ indexSearchMatchCount }}
</button>
</div>
<Button v-if="activeTab === 'indexes'" size="sm" :class="structureToolbarButtonClass" :disabled="!structureCapabilities.createIndex || indexesLoading" @click="addIndex"> <Button v-if="activeTab === 'indexes'" size="sm" :class="structureToolbarButtonClass" :disabled="!structureCapabilities.createIndex || indexesLoading" @click="addIndex">
<Plus :class="structureIconClass" /> <Plus :class="structureIconClass" />
{{ t("structureEditor.addIndex") }} {{ t("structureEditor.addIndex") }}
@@ -2345,9 +2494,9 @@ watch(activeTab, (tab) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="index in indexes" :key="index.id" :class="index.markedForDrop ? 'bg-destructive/5 opacity-60' : ''" :data-new-index-row="!index.original ? 'true' : undefined"> <tr v-for="(index, rowIndex) in indexes" :key="index.id" :class="indexRowClass(index)" :data-new-index-row="!index.original ? 'true' : undefined" :data-index-row-index="rowIndex">
<td :class="structureCellClass"> <td :class="structureCellClass">
<Input :model-value="index.name" :class="structureControlClass" :disabled="!canEditIndexDraft(index)" data-index-name-input @update:model-value="(value: string | number) => onIndexNameInput(index, value)" /> <Input :model-value="index.name" :class="[structureControlClass, indexSearchFieldClass(index, index.name)]" :disabled="!canEditIndexDraft(index)" data-index-name-input @update:model-value="(value: string | number) => onIndexNameInput(index, value)" />
</td> </td>
<td :class="[structureCellClass, 'overflow-hidden']"> <td :class="[structureCellClass, 'overflow-hidden']">
<DropdownMenu v-if="canEditIndexDraft(index)"> <DropdownMenu v-if="canEditIndexDraft(index)">
@@ -2405,10 +2554,10 @@ watch(activeTab, (tab) => {
<span v-else class="text-[length:var(--structure-font-size)] text-muted-foreground">{{ index.includedColumns.join(", ") }}</span> <span v-else class="text-[length:var(--structure-font-size)] text-muted-foreground">{{ index.includedColumns.join(", ") }}</span>
</td> </td>
<td :class="structureCellClass"> <td :class="structureCellClass">
<Input v-model="index.filter" :class="structureMonoControlClass" :placeholder="index.original?.filter || ''" :disabled="!canEditIndexFilter(index)" /> <Input v-model="index.filter" :class="[structureMonoControlClass, indexSearchFieldClass(index, index.filter)]" :placeholder="index.original?.filter || ''" :disabled="!canEditIndexFilter(index)" />
</td> </td>
<td :class="structureCellClass"> <td :class="structureCellClass">
<Input v-model="index.comment" :class="structureControlClass" :disabled="!canEditIndexComment(index)" /> <Input v-model="index.comment" :class="[structureControlClass, indexSearchFieldClass(index, index.comment)]" :disabled="!canEditIndexComment(index)" />
</td> </td>
<td :class="structureLastCellClass"> <td :class="structureLastCellClass">
<Badge v-if="index.isPrimary" variant="outline">{{ t("structureEditor.primary") }}</Badge> <Badge v-if="index.isPrimary" variant="outline">{{ t("structureEditor.primary") }}</Badge>
+6
View File
@@ -169,6 +169,8 @@ export default {
createDuckDbFile: "Create DuckDB file", createDuckDbFile: "Create DuckDB file",
createSqliteFile: "Create SQLite database", createSqliteFile: "Create SQLite database",
memoryDatabasePathHint: "Use :memory: to create an in-memory SQLite or DuckDB database.", memoryDatabasePathHint: "Use :memory: to create an in-memory SQLite or DuckDB database.",
sqliteCipherKey: "SQLCipher Key",
sqliteCipherKeyPlaceholder: "Leave empty for unencrypted SQLite",
sqliteExtensions: "SQLite Extensions", sqliteExtensions: "SQLite Extensions",
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
sqliteExtensionsHint: "One .dylib/.so/.dll per line. Use “path|entry point” when an entry point is required. Loaded on connect, then load_extension is disabled.", sqliteExtensionsHint: "One .dylib/.so/.dll per line. Use “path|entry point” when an entry point is required. Loaded on connect, then load_extension is disabled.",
@@ -1321,6 +1323,8 @@ export default {
openObjectBrowser: "Browse Objects", openObjectBrowser: "Browse Objects",
viewData: "View Data", viewData: "View Data",
editStructure: "Edit Structure", editStructure: "Edit Structure",
editColumn: "Edit Column",
editIndex: "Edit Index",
refreshChildren: "Refresh", refreshChildren: "Refresh",
expandAll: "Expand All", expandAll: "Expand All",
pin: "Pin", pin: "Pin",
@@ -1693,6 +1697,8 @@ export default {
presetFieldsTemplate: "Preset Field Set", presetFieldsTemplate: "Preset Field Set",
searchColumns: "Search column/comment", searchColumns: "Search column/comment",
nextColumnMatch: "Go to next matching column", nextColumnMatch: "Go to next matching column",
searchIndexes: "Search index",
nextIndexMatch: "Go to next matching index",
addIndex: "Add Index", addIndex: "Add Index",
addForeignKey: "Add Foreign Key", addForeignKey: "Add Foreign Key",
addTrigger: "Add Trigger", addTrigger: "Add Trigger",
+6
View File
@@ -171,6 +171,8 @@ export default withEnglishFallback({
createDuckDbFile: "Crear archivo DuckDB", createDuckDbFile: "Crear archivo DuckDB",
createSqliteFile: "Crear base de datos SQLite", createSqliteFile: "Crear base de datos SQLite",
memoryDatabasePathHint: "Usa :memory: para crear una base de datos SQLite o DuckDB en memoria.", memoryDatabasePathHint: "Usa :memory: para crear una base de datos SQLite o DuckDB en memoria.",
sqliteCipherKey: "Clave SQLCipher",
sqliteCipherKeyPlaceholder: "Déjalo vacío para SQLite sin cifrar",
sqliteExtensions: "Extensiones de SQLite", sqliteExtensions: "Extensiones de SQLite",
sqliteExtensionsPlaceholder: "/ruta/a/regexp.dylib\n/ruta/a/text.dylib|sqlite3_text_init", sqliteExtensionsPlaceholder: "/ruta/a/regexp.dylib\n/ruta/a/text.dylib|sqlite3_text_init",
sqliteExtensionsHint: 'Una .dylib/.so/.dll por línea. Usa "ruta|punto de entrada" cuando se requiera un punto de entrada. Se cargan al conectar, luego load_extension se deshabilita.', sqliteExtensionsHint: 'Una .dylib/.so/.dll por línea. Usa "ruta|punto de entrada" cuando se requiera un punto de entrada. Se cargan al conectar, luego load_extension se deshabilita.',
@@ -1279,6 +1281,8 @@ export default withEnglishFallback({
openObjectBrowser: "Explorar objetos", openObjectBrowser: "Explorar objetos",
viewData: "Ver datos", viewData: "Ver datos",
editStructure: "Editar estructura", editStructure: "Editar estructura",
editColumn: "Editar columna",
editIndex: "Editar índice",
refreshChildren: "Actualizar", refreshChildren: "Actualizar",
expandAll: "Expandir todo", expandAll: "Expandir todo",
pin: "Fijar", pin: "Fijar",
@@ -1637,6 +1641,8 @@ export default withEnglishFallback({
presetFieldsTemplate: "Conjunto de campos predefinidos", presetFieldsTemplate: "Conjunto de campos predefinidos",
searchColumns: "Buscar columna/comentario", searchColumns: "Buscar columna/comentario",
nextColumnMatch: "Ir a la siguiente columna coincidente", nextColumnMatch: "Ir a la siguiente columna coincidente",
searchIndexes: "Buscar índice",
nextIndexMatch: "Ir al siguiente índice coincidente",
addIndex: "Agregar índice", addIndex: "Agregar índice",
addForeignKey: "Agregar clave foránea", addForeignKey: "Agregar clave foránea",
addTrigger: "Agregar disparador", addTrigger: "Agregar disparador",
+6
View File
@@ -170,6 +170,8 @@ export default withEnglishFallback({
createDuckDbFile: "Crea file DuckDB", createDuckDbFile: "Crea file DuckDB",
createSqliteFile: "Crea database SQLite", createSqliteFile: "Crea database SQLite",
memoryDatabasePathHint: "Usa :memory: per creare un database SQLite o DuckDB in memoria.", memoryDatabasePathHint: "Usa :memory: per creare un database SQLite o DuckDB in memoria.",
sqliteCipherKey: "Chiave SQLCipher",
sqliteCipherKeyPlaceholder: "Lascia vuoto per SQLite non cifrato",
sqliteExtensions: "Estensioni SQLite", sqliteExtensions: "Estensioni SQLite",
sqliteExtensionsPlaceholder: "/percorso/per/regexp.dylib\n/percorso/per/text.dylib|sqlite3_text_init", sqliteExtensionsPlaceholder: "/percorso/per/regexp.dylib\n/percorso/per/text.dylib|sqlite3_text_init",
sqliteExtensionsHint: "Una .dylib/.so/.dll per riga. Usa “percorso|punto di ingresso” quando è richiesto un punto di ingresso. Caricata alla connessione, poi load_extension viene disabilitata.", sqliteExtensionsHint: "Una .dylib/.so/.dll per riga. Usa “percorso|punto di ingresso” quando è richiesto un punto di ingresso. Caricata alla connessione, poi load_extension viene disabilitata.",
@@ -1277,6 +1279,8 @@ export default withEnglishFallback({
openObjectBrowser: "Esplora Oggetti", openObjectBrowser: "Esplora Oggetti",
viewData: "Visualizza Dati", viewData: "Visualizza Dati",
editStructure: "Modifica Struttura", editStructure: "Modifica Struttura",
editColumn: "Modifica Colonna",
editIndex: "Modifica Indice",
refreshChildren: "Aggiorna", refreshChildren: "Aggiorna",
expandAll: "Espandi tutto", expandAll: "Espandi tutto",
pin: "Fissa", pin: "Fissa",
@@ -1635,6 +1639,8 @@ export default withEnglishFallback({
presetFieldsTemplate: "Set di campi predefiniti", presetFieldsTemplate: "Set di campi predefiniti",
searchColumns: "Cerca colonna/commento", searchColumns: "Cerca colonna/commento",
nextColumnMatch: "Vai alla prossima colonna corrispondente", nextColumnMatch: "Vai alla prossima colonna corrispondente",
searchIndexes: "Cerca indice",
nextIndexMatch: "Vai al prossimo indice corrispondente",
addIndex: "Aggiungi Indice", addIndex: "Aggiungi Indice",
addForeignKey: "Aggiungi Chiave Esterna", addForeignKey: "Aggiungi Chiave Esterna",
addTrigger: "Aggiungi Trigger", addTrigger: "Aggiungi Trigger",
+6
View File
@@ -170,6 +170,8 @@ export default withEnglishFallback({
createDuckDbFile: "DuckDBファイルを作成", createDuckDbFile: "DuckDBファイルを作成",
createSqliteFile: "SQLiteデータベースを作成", createSqliteFile: "SQLiteデータベースを作成",
memoryDatabasePathHint: "インメモリのSQLiteまたはDuckDBデータベースを作成するには :memory: を使用してください。", memoryDatabasePathHint: "インメモリのSQLiteまたはDuckDBデータベースを作成するには :memory: を使用してください。",
sqliteCipherKey: "SQLCipherキー",
sqliteCipherKeyPlaceholder: "暗号化されていないSQLiteでは空のままにします",
sqliteExtensions: "SQLite拡張", sqliteExtensions: "SQLite拡張",
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
sqliteExtensionsHint: "1行に1つの.dylib/.so/.dllファイルを指定します。エントリポイントが必要な場合は「パス|エントリポイント」形式で指定します。接続時に読み込まれ、その後load_extensionは無効になります。", sqliteExtensionsHint: "1行に1つの.dylib/.so/.dllファイルを指定します。エントリポイントが必要な場合は「パス|エントリポイント」形式で指定します。接続時に読み込まれ、その後load_extensionは無効になります。",
@@ -1275,6 +1277,8 @@ export default withEnglishFallback({
openObjectBrowser: "オブジェクトを参照", openObjectBrowser: "オブジェクトを参照",
viewData: "データを表示", viewData: "データを表示",
editStructure: "構造を編集", editStructure: "構造を編集",
editColumn: "列を編集",
editIndex: "インデックスを編集",
refreshChildren: "更新", refreshChildren: "更新",
expandAll: "すべて展開", expandAll: "すべて展開",
pin: "ピン留め", pin: "ピン留め",
@@ -1668,6 +1672,8 @@ export default withEnglishFallback({
presetFieldsTemplate: "プリセット列セット", presetFieldsTemplate: "プリセット列セット",
searchColumns: "列/コメントを検索", searchColumns: "列/コメントを検索",
nextColumnMatch: "次の一致する列へ移動", nextColumnMatch: "次の一致する列へ移動",
searchIndexes: "インデックスを検索",
nextIndexMatch: "次の一致するインデックスへ移動",
addIndex: "インデックスを追加", addIndex: "インデックスを追加",
addForeignKey: "外部キーを追加", addForeignKey: "外部キーを追加",
addTrigger: "トリガーを追加", addTrigger: "トリガーを追加",
+6
View File
@@ -171,6 +171,8 @@ export default withEnglishFallback({
createDuckDbFile: "Criar arquivo DuckDB", createDuckDbFile: "Criar arquivo DuckDB",
createSqliteFile: "Criar banco de dados SQLite", createSqliteFile: "Criar banco de dados SQLite",
memoryDatabasePathHint: "Use :memory: para criar um banco de dados SQLite ou DuckDB na memória.", memoryDatabasePathHint: "Use :memory: para criar um banco de dados SQLite ou DuckDB na memória.",
sqliteCipherKey: "Chave SQLCipher",
sqliteCipherKeyPlaceholder: "Deixe vazio para SQLite não criptografado",
sqliteExtensions: "Extensões SQLite", sqliteExtensions: "Extensões SQLite",
sqliteExtensionsPlaceholder: "/caminho/para/regexp.dylib\n/caminho/para/text.dylib|sqlite3_text_init", sqliteExtensionsPlaceholder: "/caminho/para/regexp.dylib\n/caminho/para/text.dylib|sqlite3_text_init",
sqliteExtensionsHint: "Um .dylib/.so/.dll por linha. Use “caminho|ponto de entrada” quando um ponto de entrada for necessário. Carregado ao conectar, depois load_extension é desabilitado.", sqliteExtensionsHint: "Um .dylib/.so/.dll por linha. Use “caminho|ponto de entrada” quando um ponto de entrada for necessário. Carregado ao conectar, depois load_extension é desabilitado.",
@@ -1278,6 +1280,8 @@ export default withEnglishFallback({
openObjectBrowser: "Explorar Objetos", openObjectBrowser: "Explorar Objetos",
viewData: "Ver Dados", viewData: "Ver Dados",
editStructure: "Editar Estrutura", editStructure: "Editar Estrutura",
editColumn: "Editar Coluna",
editIndex: "Editar Índice",
refreshChildren: "Atualizar", refreshChildren: "Atualizar",
expandAll: "Expandir tudo", expandAll: "Expandir tudo",
pin: "Fixar", pin: "Fixar",
@@ -1636,6 +1640,8 @@ export default withEnglishFallback({
presetFieldsTemplate: "Conjunto de campos predefinidos", presetFieldsTemplate: "Conjunto de campos predefinidos",
searchColumns: "Pesquisar coluna/comentário", searchColumns: "Pesquisar coluna/comentário",
nextColumnMatch: "Ir para a próxima coluna correspondente", nextColumnMatch: "Ir para a próxima coluna correspondente",
searchIndexes: "Pesquisar índice",
nextIndexMatch: "Ir para o próximo índice correspondente",
addIndex: "Adicionar índice", addIndex: "Adicionar índice",
addForeignKey: "Adicionar chave estrangeira", addForeignKey: "Adicionar chave estrangeira",
addTrigger: "Adicionar trigger", addTrigger: "Adicionar trigger",
+6
View File
@@ -171,6 +171,8 @@ export default withEnglishFallback({
createDuckDbFile: "新建 DuckDB 文件", createDuckDbFile: "新建 DuckDB 文件",
createSqliteFile: "新建 SQLite 数据库", createSqliteFile: "新建 SQLite 数据库",
memoryDatabasePathHint: "输入 :memory: 可创建 SQLite 或 DuckDB 内存数据库。", memoryDatabasePathHint: "输入 :memory: 可创建 SQLite 或 DuckDB 内存数据库。",
sqliteCipherKey: "SQLCipher 密码(密钥)",
sqliteCipherKeyPlaceholder: "未加密 SQLite 留空",
sqliteExtensions: "SQLite 扩展库", sqliteExtensions: "SQLite 扩展库",
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
sqliteExtensionsHint: "每行一个 .dylib/.so/.dll;需要入口函数时使用“路径|入口函数”。连接时加载,加载后会关闭 load_extension。", sqliteExtensionsHint: "每行一个 .dylib/.so/.dll;需要入口函数时使用“路径|入口函数”。连接时加载,加载后会关闭 load_extension。",
@@ -1323,6 +1325,8 @@ export default withEnglishFallback({
openObjectBrowser: "浏览对象", openObjectBrowser: "浏览对象",
viewData: "查看数据", viewData: "查看数据",
editStructure: "编辑表结构", editStructure: "编辑表结构",
editColumn: "编辑字段",
editIndex: "编辑索引",
refreshChildren: "刷新", refreshChildren: "刷新",
expandAll: "展开全部", expandAll: "展开全部",
pin: "置顶", pin: "置顶",
@@ -1693,6 +1697,8 @@ export default withEnglishFallback({
presetFieldsTemplate: "预设字段组合", presetFieldsTemplate: "预设字段组合",
searchColumns: "搜索字段/注释", searchColumns: "搜索字段/注释",
nextColumnMatch: "定位下一个匹配字段", nextColumnMatch: "定位下一个匹配字段",
searchIndexes: "搜索索引",
nextIndexMatch: "定位下一个匹配索引",
addIndex: "新增索引", addIndex: "新增索引",
addForeignKey: "新增外键", addForeignKey: "新增外键",
addTrigger: "新增触发器", addTrigger: "新增触发器",
+6
View File
@@ -171,6 +171,8 @@ export default withEnglishFallback({
createDuckDbFile: "建立 DuckDB 檔案", createDuckDbFile: "建立 DuckDB 檔案",
createSqliteFile: "建立 SQLite 資料庫", createSqliteFile: "建立 SQLite 資料庫",
memoryDatabasePathHint: "輸入 :memory: 可建立 SQLite 或 DuckDB 記憶體資料庫。", memoryDatabasePathHint: "輸入 :memory: 可建立 SQLite 或 DuckDB 記憶體資料庫。",
sqliteCipherKey: "SQLCipher 密碼(密鑰)",
sqliteCipherKeyPlaceholder: "未加密 SQLite 留空",
sqliteExtensions: "SQLite 擴充庫", sqliteExtensions: "SQLite 擴充庫",
sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init", sqliteExtensionsPlaceholder: "/path/to/regexp.dylib\n/path/to/text.dylib|sqlite3_text_init",
sqliteExtensionsHint: "每行一個 .dylib/.so/.dll;需要入口函式時使用「路徑|入口函式」。連線時載入,載入後會關閉 load_extension。", sqliteExtensionsHint: "每行一個 .dylib/.so/.dll;需要入口函式時使用「路徑|入口函式」。連線時載入,載入後會關閉 load_extension。",
@@ -1278,6 +1280,8 @@ export default withEnglishFallback({
openObjectBrowser: "瀏覽物件", openObjectBrowser: "瀏覽物件",
viewData: "檢視資料", viewData: "檢視資料",
editStructure: "編輯資料表結構", editStructure: "編輯資料表結構",
editColumn: "編輯欄位",
editIndex: "編輯索引",
refreshChildren: "重新整理", refreshChildren: "重新整理",
expandAll: "展開全部", expandAll: "展開全部",
pin: "置頂", pin: "置頂",
@@ -1591,6 +1595,8 @@ export default withEnglishFallback({
presetFieldsTemplate: "預設欄位組合", presetFieldsTemplate: "預設欄位組合",
searchColumns: "搜尋欄位/註解", searchColumns: "搜尋欄位/註解",
nextColumnMatch: "定位下一個符合欄位", nextColumnMatch: "定位下一個符合欄位",
searchIndexes: "搜尋索引",
nextIndexMatch: "定位下一個符合索引",
addIndex: "新增索引", addIndex: "新增索引",
addForeignKey: "新增外鍵", addForeignKey: "新增外鍵",
addTrigger: "新增觸發器", addTrigger: "新增觸發器",
+9 -7
View File
@@ -2,7 +2,7 @@ import { defineStore } from "pinia";
import { uuid } from "@/lib/common/utils"; import { uuid } from "@/lib/common/utils";
import { markRaw, ref, watch, computed } from "vue"; import { markRaw, ref, watch, computed } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import type { DatabaseType, QueryResult, QueryTab, TableInfoTab } from "@/types/database"; import type { DatabaseType, QueryResult, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
import { orderPinnedFirst } from "@/lib/app/pinnedItems"; import { orderPinnedFirst } from "@/lib/app/pinnedItems";
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState"; import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/diagram/explainPlan"; import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/diagram/explainPlan";
@@ -885,18 +885,19 @@ export const useQueryStore = defineStore("query", () => {
return id; return id;
} }
function applyTableStructureInitialTab(tab: QueryTab, initialTab?: TableInfoTab) { function applyTableStructureInitialTab(tab: QueryTab, initialTab?: TableInfoTab, initialTarget?: TableStructureEditorTarget) {
if (!initialTab) return; if (!initialTab && !initialTarget?.name) return;
tab.structureInitialTab = initialTab; if (initialTab) tab.structureInitialTab = initialTab;
tab.structureInitialTarget = initialTarget?.name ? initialTarget : undefined;
tab.structureInitialTabRequestId = (tab.structureInitialTabRequestId ?? 0) + 1; tab.structureInitialTabRequestId = (tab.structureInitialTabRequestId ?? 0) + 1;
} }
function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string, initialTab?: TableInfoTab) { function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string, initialTab?: TableInfoTab, initialTarget?: TableStructureEditorTarget) {
const resolvedTableName = tableName || ""; const resolvedTableName = tableName || "";
if (resolvedTableName) { if (resolvedTableName) {
const existing = tabs.value.find((tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.structureTableName || "") === resolvedTableName); const existing = tabs.value.find((tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.structureTableName || "") === resolvedTableName);
if (existing) { if (existing) {
applyTableStructureInitialTab(existing, initialTab); applyTableStructureInitialTab(existing, initialTab, initialTarget);
activeTabId.value = existing.id; activeTabId.value = existing.id;
return existing.id; return existing.id;
} }
@@ -917,7 +918,8 @@ export const useQueryStore = defineStore("query", () => {
mode: "structure", mode: "structure",
structureTableName: resolvedTableName, structureTableName: resolvedTableName,
structureInitialTab: initialTab, structureInitialTab: initialTab,
structureInitialTabRequestId: initialTab ? 1 : undefined, structureInitialTabRequestId: initialTab || initialTarget?.name ? 1 : undefined,
structureInitialTarget: initialTarget?.name ? initialTarget : undefined,
}; };
tabs.value.push(tab); tabs.value.push(tab);
activeTabId.value = id; activeTabId.value = id;
+6
View File
@@ -612,6 +612,11 @@ export interface TreeNode {
export type TableInfoTab = "columns" | "indexes" | "foreignKeys" | "triggers" | "ddl"; export type TableInfoTab = "columns" | "indexes" | "foreignKeys" | "triggers" | "ddl";
export interface TableStructureEditorTarget {
kind: "column" | "index";
name: string;
}
export interface TableStructureEditorDraft { export interface TableStructureEditorDraft {
activeTab: TableInfoTab; activeTab: TableInfoTab;
newTableName: string; newTableName: string;
@@ -687,6 +692,7 @@ export interface QueryTab {
structureTableName?: string; structureTableName?: string;
structureInitialTab?: TableInfoTab; structureInitialTab?: TableInfoTab;
structureInitialTabRequestId?: number; structureInitialTabRequestId?: number;
structureInitialTarget?: TableStructureEditorTarget;
structureDraft?: TableStructureEditorDraft; structureDraft?: TableStructureEditorDraft;
objectBrowser?: { objectBrowser?: {
schema?: string; schema?: string;
+10 -3
View File
@@ -3526,19 +3526,26 @@ test("reopening table structure tabs records the requested initial tab", () => {
setActivePinia(createPinia()); setActivePinia(createPinia());
const store = useQueryStore(); const store = useQueryStore();
const structureId = store.openTableStructure("conn-1", "db", "public", "users", "indexes"); const structureId = store.openTableStructure("conn-1", "db", "public", "users", "indexes", { kind: "index", name: "idx_users_email" });
const firstTab = store.tabs.find((item) => item.id === structureId); const firstTab = store.tabs.find((item) => item.id === structureId);
assert.equal(firstTab?.structureInitialTab, "indexes"); assert.equal(firstTab?.structureInitialTab, "indexes");
assert.equal(firstTab?.structureInitialTabRequestId, 1); assert.equal(firstTab?.structureInitialTabRequestId, 1);
assert.deepEqual(firstTab?.structureInitialTarget, { kind: "index", name: "idx_users_email" });
const reusedStructureId = store.openTableStructure("conn-1", "db", "public", "users", "foreignKeys"); const reusedStructureId = store.openTableStructure("conn-1", "db", "public", "users", "columns", { kind: "column", name: "email" });
const reusedTab = store.tabs.find((item) => item.id === reusedStructureId); const reusedTab = store.tabs.find((item) => item.id === reusedStructureId);
assert.equal(reusedStructureId, structureId); assert.equal(reusedStructureId, structureId);
assert.equal(reusedTab?.structureInitialTab, "foreignKeys"); assert.equal(reusedTab?.structureInitialTab, "columns");
assert.equal(reusedTab?.structureInitialTabRequestId, 2); assert.equal(reusedTab?.structureInitialTabRequestId, 2);
assert.deepEqual(reusedTab?.structureInitialTarget, { kind: "column", name: "email" });
assert.equal(store.activeTabId, structureId); assert.equal(store.activeTabId, structureId);
store.openTableStructure("conn-1", "db", "public", "users", "foreignKeys");
assert.equal(reusedTab?.structureInitialTab, "foreignKeys");
assert.equal(reusedTab?.structureInitialTabRequestId, 3);
assert.equal(reusedTab?.structureInitialTarget, undefined);
} finally { } finally {
restoreStorage(); restoreStorage();
} }