feat(structure): open editor from field and index nodes
This commit is contained in:
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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: "トリガーを追加",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: "新增触发器",
|
||||||
|
|||||||
@@ -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: "新增觸發器",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user