feat: show username/password fields for Access database connections

This commit is contained in:
t8y2 committed 2026-06-10 08:37:18 +08:00
1 parent 2e021d37d1
commit edba8214b5
477 files changed
+5720 -19773

No files matched your search

@@ -46,28 +46,12 @@ import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDia
import * as api from "@/lib/api";
import type { ConnectionConfig, ObjectInfo, ObjectSourceKind } from "@/types/database";
import { isSchemaAware } from "@/lib/databaseCapabilities";
import {
supportsSchemaDiagram,
supportsTableImport,
supportsTableStructureEditing,
supportsTableTruncate,
} from "@/lib/databaseFeatureSupport";
import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/databaseFeatureSupport";
import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
import { buildTableSelectSql } from "@/lib/tableSelectSql";
import {
buildDropObjectSql,
buildDuplicateTableStructureSql,
buildEmptyTableSql,
buildTruncateTableSql,
type TableAdminSqlOptions,
} from "@/lib/dbAdminSql";
import { buildDropObjectSql, buildDuplicateTableStructureSql, buildEmptyTableSql, buildTruncateTableSql, type TableAdminSqlOptions } from "@/lib/dbAdminSql";
import { useToast } from "@/composables/useToast";
import {
buildExecutableObjectSourceStatements,
buildRoutineRenameObjectSourceStatements,
objectSourceSaveExecutionMode,
supportsSourceBackedRoutineRename,
} from "@/lib/objectSourceEditor";
import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, objectSourceSaveExecutionMode, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/objectRenameSql";
import { buildViewDdl } from "@/lib/viewDdl";
import { isTauriRuntime } from "@/lib/tauriRuntime";
@@ -83,16 +67,7 @@ import QueryEditor from "@/components/editor/QueryEditor.vue";
import type { SqlFormatDialect } from "@/lib/sqlFormatter";
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import {
buildObjectBrowserRows,
filterObjectBrowserRows,
formatObjectBrowserTimestamp,
initialObjectBrowserSortDirection,
sortObjectBrowserRows,
type ObjectBrowserRow,
type ObjectBrowserSortDirection,
type ObjectBrowserSortKey,
} from "@/lib/objectBrowserRows";
import { buildObjectBrowserRows, filterObjectBrowserRows, formatObjectBrowserTimestamp, initialObjectBrowserSortDirection, sortObjectBrowserRows, type ObjectBrowserRow, type ObjectBrowserSortDirection, type ObjectBrowserSortKey } from "@/lib/objectBrowserRows";
type ObjectFilter = "all" | "tables" | "views" | "procedures" | "functions" | "sequences" | "packages";
@@ -130,9 +105,7 @@ const sourceContent = ref("");
const sourceError = ref("");
const sourceRow = ref<ObjectBrowserRow | null>(null);
const sourceEditing = ref(false);
const effectiveDatabaseType = computed(
() => effectiveDatabaseTypeForConnection(props.connection) ?? props.connection.db_type,
);
const effectiveDatabaseType = computed(() => effectiveDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
const sourceDraft = ref("");
const sourceSaving = ref(false);
const sourceSaveError = ref("");
@@ -164,29 +137,19 @@ let loadId = 0;
// Export via background tracker
const { addTask: addExportTask } = useExportTracker();
const needsSchema = computed(
() => isSchemaAware(props.connection.db_type) && !connectionUsesDatabaseObjectTreeMode(props.connection),
);
const needsSchema = computed(() => isSchemaAware(props.connection.db_type) && !connectionUsesDatabaseObjectTreeMode(props.connection));
const tableCount = computed(() => rows.value.filter((row) => row.type === "TABLE").length);
const viewCount = computed(() => rows.value.filter((row) => row.type === "VIEW").length);
const procedureCount = computed(() => rows.value.filter((row) => row.type === "PROCEDURE").length);
const functionCount = computed(() => rows.value.filter((row) => row.type === "FUNCTION").length);
const sequenceCount = computed(() => rows.value.filter((row) => row.type === "SEQUENCE").length);
const packageCount = computed(
() => rows.value.filter((row) => row.type === "PACKAGE" || row.type === "PACKAGE_BODY").length,
);
const packageCount = computed(() => rows.value.filter((row) => row.type === "PACKAGE" || row.type === "PACKAGE_BODY").length);
const canOpenStructureEditor = computed(() => supportsTableStructureEditing(effectiveDatabaseType.value));
const canOpenDiagram = computed(() => !!props.database && supportsSchemaDiagram(effectiveDatabaseType.value));
const canOpenTableImport = computed(() => !!props.database && supportsTableImport(effectiveDatabaseType.value));
const supportsTruncateTable = computed(() => supportsTableTruncate(effectiveDatabaseType.value));
const sourceDialect = computed<"mysql" | "postgres" | "sqlserver">(() => {
if (
effectiveDatabaseType.value === "postgres" ||
effectiveDatabaseType.value === "gaussdb" ||
effectiveDatabaseType.value === "kwdb" ||
effectiveDatabaseType.value === "opengauss"
)
return "postgres";
if (effectiveDatabaseType.value === "postgres" || effectiveDatabaseType.value === "gaussdb" || effectiveDatabaseType.value === "kwdb" || effectiveDatabaseType.value === "opengauss") return "postgres";
if (effectiveDatabaseType.value === "sqlserver") return "sqlserver";
return "mysql";
});
@@ -252,11 +215,7 @@ const selectedTableRows = computed(() => {
return selectableRows.value.filter((row) => ids.has(row.id));
});
const selectedTableCount = computed(() => selectedTableRows.value.length);
const allVisibleTablesSelected = computed(
() =>
visibleSelectableRows.value.length > 0 &&
visibleSelectableRows.value.every((row) => selectedTableIds.value.has(row.id)),
);
const allVisibleTablesSelected = computed(() => visibleSelectableRows.value.length > 0 && visibleSelectableRows.value.every((row) => selectedTableIds.value.has(row.id)));
function iconFor(row: ObjectBrowserRow) {
if (row.type === "VIEW") return Eye;
@@ -307,9 +266,7 @@ function groupedFilteredRows() {
const candidateIds = new Set(candidateRows.map((row) => row.id));
const matchingRows = filterObjectBrowserRows(candidateRows, query);
const matchingIds = new Set(matchingRows.map((row) => row.id));
const parentIdsWithMatchingPartitions = new Set(
matchingRows.flatMap((row) => (row.partitionParentId ? [row.partitionParentId] : [])),
);
const parentIdsWithMatchingPartitions = new Set(matchingRows.flatMap((row) => (row.partitionParentId ? [row.partitionParentId] : [])));
const rootRows = candidateRows.filter((row) => {
if (row.partitionParentId) return false;
if (!query) return true;
@@ -325,8 +282,7 @@ function groupedFilteredRows() {
const parentMatches = matchingIds.has(row.id);
const shouldShowPartitions = expandedPartitionParentIds.value.has(row.id) || !!query;
if (!shouldShowPartitions) continue;
const visiblePartitions =
query && !parentMatches ? partitions.filter((partition) => matchingIds.has(partition.id)) : partitions;
const visiblePartitions = query && !parentMatches ? partitions.filter((partition) => matchingIds.has(partition.id)) : partitions;
result.push(...sortObjectBrowserRows(visiblePartitions, sortKey.value, sortDirection.value));
}
@@ -355,21 +311,11 @@ function togglePartitionParent(row: ObjectBrowserRow) {
}
function canOpenSource(row: ObjectBrowserRow) {
return (
row.type === "VIEW" ||
row.type === "PROCEDURE" ||
row.type === "FUNCTION" ||
row.type === "SEQUENCE" ||
row.type === "PACKAGE" ||
row.type === "PACKAGE_BODY"
);
return row.type === "VIEW" || row.type === "PROCEDURE" || row.type === "FUNCTION" || row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY";
}
function canRename(row: ObjectBrowserRow) {
return (
supportsObjectRename(effectiveDatabaseType.value, row.type) ||
supportsSourceBackedRoutineRename(effectiveDatabaseType.value, row.type as ObjectSourceKind)
);
return supportsObjectRename(effectiveDatabaseType.value, row.type) || supportsSourceBackedRoutineRename(effectiveDatabaseType.value, row.type as ObjectSourceKind);
}
function sourceTitle(row: ObjectBrowserRow | null) {
@@ -405,13 +351,7 @@ async function openSource(row: ObjectBrowserRow) {
sourceSaveError.value = "";
sourceLoading.value = true;
try {
const result = await api.getObjectSource(
props.connection.id,
props.database,
row.schema || selectedSchema.value || props.database,
row.name,
row.type as ObjectSourceKind,
);
const result = await api.getObjectSource(props.connection.id, props.database, row.schema || selectedSchema.value || props.database, row.name, row.type as ObjectSourceKind);
sourceContent.value = result.source;
sourceDraft.value = result.source;
sourceEditing.value = row.type !== "SEQUENCE";
@@ -425,13 +365,7 @@ async function openSource(row: ObjectBrowserRow) {
async function openViewDdl(row: ObjectBrowserRow) {
if (row.type !== "VIEW") return;
try {
const result = await api.getObjectSource(
props.connection.id,
props.database,
row.schema || selectedSchema.value || props.database,
row.name,
"VIEW",
);
const result = await api.getObjectSource(props.connection.id, props.database, row.schema || selectedSchema.value || props.database, row.name, "VIEW");
const ddl = await buildViewDdl({
databaseType: effectiveDatabaseType.value,
schema: row.schema || selectedSchema.value || props.database,
@@ -534,13 +468,7 @@ async function confirmRename() {
try {
const schema = row.schema || selectedSchema.value || props.database;
if (supportsSourceBackedRoutineRename(effectiveDatabaseType.value, row.type as ObjectSourceKind)) {
const source = await api.getObjectSource(
props.connection.id,
props.database,
schema,
row.name,
row.type as ObjectSourceKind,
);
const source = await api.getObjectSource(props.connection.id, props.database, schema, row.name, row.type as ObjectSourceKind);
const statements = await buildRoutineRenameObjectSourceStatements({
databaseType: effectiveDatabaseType.value,
objectType: row.type as ObjectSourceKind,
@@ -566,11 +494,7 @@ async function confirmRename() {
showRenameDialog.value = false;
if (sourceRow.value?.id === row.id) closeSource();
await reload();
await connectionStore.refreshObjectListTreeNode(
props.connection.id,
props.database,
row.schema || selectedSchema.value,
);
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, row.schema || selectedSchema.value);
} catch (e: any) {
renameError.value = e?.message || String(e);
}
@@ -587,21 +511,10 @@ async function confirmDrop() {
name: row.name,
});
await api.executeQuery(props.connection.id, props.database, sql);
const successKey =
row.type === "VIEW"
? "contextMenu.dropViewSuccess"
: row.type === "PROCEDURE"
? "contextMenu.dropProcedureSuccess"
: row.type === "FUNCTION"
? "contextMenu.dropFunctionSuccess"
: "contextMenu.dropTableSuccess";
const successKey = row.type === "VIEW" ? "contextMenu.dropViewSuccess" : row.type === "PROCEDURE" ? "contextMenu.dropProcedureSuccess" : row.type === "FUNCTION" ? "contextMenu.dropFunctionSuccess" : "contextMenu.dropTableSuccess";
toast(t(successKey, { name: row.name }));
await reload();
await connectionStore.refreshObjectListTreeNode(
props.connection.id,
props.database,
row.schema || selectedSchema.value,
);
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, row.schema || selectedSchema.value);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -798,17 +711,8 @@ async function exportStructure(row: ObjectBrowserRow) {
async function exportDataLegacy(row: ObjectBrowserRow, format: "json" | "sql") {
try {
const schema = row.schema || selectedSchema.value;
const tableColumns =
format === "sql"
? await api.getColumns(props.connection.id, props.database, schema || props.database, row.name)
: undefined;
const queryColumns =
props.connection.db_type === "neo4j"
? (
tableColumns ??
(await api.getColumns(props.connection.id, props.database, schema || props.database, row.name))
).map((column) => column.name)
: undefined;
const tableColumns = format === "sql" ? await api.getColumns(props.connection.id, props.database, schema || props.database, row.name) : undefined;
const queryColumns = props.connection.db_type === "neo4j" ? (tableColumns ?? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name))).map((column) => column.name) : undefined;
const result = await fetchTableDataForExport({
databaseType: effectiveDatabaseType.value,
schema,
@@ -848,10 +752,7 @@ async function exportDataLegacy(row: ObjectBrowserRow, format: "json" | "sql") {
}
}
function columnTypesForResultColumns(
columns: string[],
tableColumns: Array<{ name: string; data_type: string }>,
): Array<string | undefined> {
function columnTypesForResultColumns(columns: string[], tableColumns: Array<{ name: string; data_type: string }>): Array<string | undefined> {
const typesByName = new Map(tableColumns.map((column) => [column.name.toLocaleLowerCase(), column.data_type]));
return columns.map((column) => typesByName.get(column.toLocaleLowerCase()));
}
@@ -896,12 +797,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx") {
let task: ExportTask | null = null;
try {
const queryColumns =
props.connection.db_type === "neo4j"
? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name)).map(
(column) => column.name,
)
: undefined;
const queryColumns = props.connection.db_type === "neo4j" ? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name)).map((column) => column.name) : undefined;
task = addExportTask(row.name, format, filePath);
const currentTask = task;
@@ -1118,11 +1014,7 @@ async function loadObjects() {
});
const availableTableIds = new Set(rows.value.filter((row) => row.type === "TABLE").map((row) => row.id));
setSelectedTableIds(new Set([...selectedTableIds.value].filter((id) => availableTableIds.has(id))));
expandedPartitionParentIds.value = new Set(
[...expandedPartitionParentIds.value].filter((id) =>
rows.value.some((row) => row.id === id && row.partitionCount),
),
);
expandedPartitionParentIds.value = new Set([...expandedPartitionParentIds.value].filter((id) => rows.value.some((row) => row.id === id && row.partitionCount)));
} catch (e: any) {
if (id !== loadId) return;
error.value = e?.message || String(e);
@@ -1160,20 +1052,7 @@ function filterCount(filter: ObjectFilter) {
}
function filterLabel(filter: ObjectFilter) {
const key =
filter === "tables"
? "objects.tables"
: filter === "views"
? "objects.views"
: filter === "procedures"
? "objects.procedures"
: filter === "functions"
? "objects.functions"
: filter === "sequences"
? "objects.sequences"
: filter === "packages"
? "objects.packages"
: "objects.all";
const key = filter === "tables" ? "objects.tables" : filter === "views" ? "objects.views" : filter === "procedures" ? "objects.procedures" : filter === "functions" ? "objects.functions" : filter === "sequences" ? "objects.sequences" : filter === "packages" ? "objects.packages" : "objects.all";
return `${t(key)} ${filterCount(filter)}`;
}
@@ -1227,17 +1106,11 @@ function exportDataSubmenu(item: ObjectBrowserRow): ContextMenuItem {
function getTableMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
return [
{ label: t("contextMenu.viewData"), action: () => openRow(item), icon: Table2 },
...(canOpenStructureEditor.value
? [{ label: t("contextMenu.editStructure"), action: () => openStructureEditor(item), icon: PencilRuler }]
: []),
...(canRename(item)
? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }]
: []),
...(canOpenStructureEditor.value ? [{ label: t("contextMenu.editStructure"), action: () => openStructureEditor(item), icon: PencilRuler }] : []),
...(canRename(item) ? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }] : []),
{ label: t("contextMenu.newQuery"), action: () => openNewQuery(item), icon: TerminalSquare },
...(canOpenDiagram.value ? [{ label: t("diagram.open"), action: () => openDiagram(item), icon: Network }] : []),
...(canOpenTableImport.value
? [{ label: t("contextMenu.importData"), action: () => openTableImport(item), icon: Download }]
: []),
...(canOpenTableImport.value ? [{ label: t("contextMenu.importData"), action: () => openTableImport(item), icon: Download }] : []),
{ label: t("dataCompare.title"), action: () => openDataCompare(item), icon: ArrowRightLeft },
{ label: "", separator: true },
exportDataSubmenu(item),
@@ -1279,9 +1152,7 @@ function getViewMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
{ label: t("contextMenu.editView"), action: () => openSource(item), icon: PencilLine },
{ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 },
{ label: t("contextMenu.viewDdl"), action: () => openViewDdl(item), icon: ScrollText },
...(canRename(item)
? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }]
: []),
...(canRename(item) ? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }] : []),
{ label: t("contextMenu.newQuery"), action: () => openNewQuery(item), icon: TerminalSquare },
...(canOpenDiagram.value ? [{ label: t("diagram.open"), action: () => openDiagram(item), icon: Network }] : []),
{ label: "", separator: true },
@@ -1302,13 +1173,9 @@ function getViewMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
function getProcFuncMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
return [
...(item.type === "PROCEDURE"
? [{ label: t("contextMenu.executeProcedure"), action: () => openProcedureExecution(item), icon: Play }]
: []),
...(item.type === "PROCEDURE" ? [{ label: t("contextMenu.executeProcedure"), action: () => openProcedureExecution(item), icon: Play }] : []),
{ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 },
...(canRename(item)
? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }]
: []),
...(canRename(item) ? [{ label: t("contextMenu.renameObject"), action: () => requestRename(item), icon: Pencil }] : []),
{ label: "", separator: true },
{
label: item.type === "PROCEDURE" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction"),
@@ -1346,19 +1213,11 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<div class="min-w-0 truncate text-sm font-medium">
{{ props.database }}<template v-if="selectedSchema"> / {{ selectedSchema }}</template>
</div>
<div class="shrink-0 rounded border bg-muted/40 px-1.5 py-0.5 text-xs text-muted-foreground">
{{ filteredRows.length }} / {{ rows.length }}
</div>
<div class="shrink-0 rounded border bg-muted/40 px-1.5 py-0.5 text-xs text-muted-foreground">{{ filteredRows.length }} / {{ rows.length }}</div>
</div>
<div class="flex min-w-[240px] flex-1 items-center gap-2">
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<Input
v-model="search"
data-object-search-input
class="h-7 text-xs"
:placeholder="t('objects.search')"
@keydown="onSearchKeydown"
/>
<Input v-model="search" data-object-search-input class="h-7 text-xs" :placeholder="t('objects.search')" @keydown="onSearchKeydown" />
<div v-if="showObjectFilter" class="flex h-7 shrink-0 items-center rounded border bg-muted/20 p-0.5">
<button
v-for="filter in objectFilters"
@@ -1375,12 +1234,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
</button>
</div>
</div>
<Select
v-if="needsSchema"
:model-value="selectedSchema"
:disabled="loadingSchemas"
@update:model-value="onSchemaChange"
>
<Select v-if="needsSchema" :model-value="selectedSchema" :disabled="loadingSchemas" @update:model-value="onSchemaChange">
<SelectTrigger class="h-7 w-36 text-xs">
<SelectValue :placeholder="loadingSchemas ? t('objects.loadingSchemas') : t('objects.schema')" />
</SelectTrigger>
@@ -1417,23 +1271,12 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<div v-else-if="error" class="flex flex-1 items-center justify-center px-6 text-center text-sm text-destructive">
{{ error }}
</div>
<div
v-else-if="filteredRows.length === 0"
class="flex flex-1 items-center justify-center text-sm text-muted-foreground"
>
<div v-else-if="filteredRows.length === 0" class="flex flex-1 items-center justify-center text-sm text-muted-foreground">
{{ t("objects.empty") }}
</div>
<div v-else class="flex min-h-0 flex-1 flex-col">
<div
class="grid h-8 shrink-0 items-center gap-3 border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground"
:style="{ gridTemplateColumns }"
>
<button
class="flex h-6 w-6 items-center justify-center rounded-sm hover:bg-accent"
type="button"
:disabled="visibleSelectableRows.length === 0"
@click="toggleVisibleTableSelection"
>
<div class="grid h-8 shrink-0 items-center gap-3 border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground" :style="{ gridTemplateColumns }">
<button class="flex h-6 w-6 items-center justify-center rounded-sm hover:bg-accent" type="button" :disabled="visibleSelectableRows.length === 0" @click="toggleVisibleTableSelection">
<CheckSquare v-if="allVisibleTablesSelected" class="h-3.5 w-3.5 text-primary" />
<Square v-else class="h-3.5 w-3.5" />
</button>
@@ -1445,42 +1288,20 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<span class="truncate">{{ t("objects.type") }}</span>
<component :is="sortIconFor('type')" v-if="sortIconFor('type')" class="h-3 w-3 shrink-0" />
</button>
<button
v-if="hasCreatedAt"
class="flex min-w-0 items-center gap-1 truncate text-left"
type="button"
@click="toggleSort('created_at')"
>
<button v-if="hasCreatedAt" class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('created_at')">
<span class="truncate">{{ t("objects.createdAt") }}</span>
<component :is="sortIconFor('created_at')" v-if="sortIconFor('created_at')" class="h-3 w-3 shrink-0" />
</button>
<button
v-if="hasUpdatedAt"
class="flex min-w-0 items-center gap-1 truncate text-left"
type="button"
@click="toggleSort('updated_at')"
>
<button v-if="hasUpdatedAt" class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('updated_at')">
<span class="truncate">{{ t("objects.updatedAt") }}</span>
<component :is="sortIconFor('updated_at')" v-if="sortIconFor('updated_at')" class="h-3 w-3 shrink-0" />
</button>
<button
v-if="hasComments"
class="flex min-w-0 items-center gap-1 truncate text-left"
type="button"
@click="toggleSort('comment')"
>
<button v-if="hasComments" class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('comment')">
<span class="truncate">{{ t("objects.comment") }}</span>
<component :is="sortIconFor('comment')" v-if="sortIconFor('comment')" class="h-3 w-3 shrink-0" />
</button>
</div>
<RecycleScroller
class="object-browser-scroller min-h-0 flex-1"
:items="filteredRows"
:item-size="38"
:buffer="600"
:skip-hover="true"
key-field="id"
>
<RecycleScroller class="object-browser-scroller min-h-0 flex-1" :items="filteredRows" :item-size="38" :buffer="600" :skip-hover="true" key-field="id">
<template #default="{ item }">
<CustomContextMenu :items="getObjectBrowserMenuItems(item)" v-slot="{ onContextMenu }">
<div
@@ -1493,12 +1314,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
@click="onRowClick(item, $event)"
@contextmenu="onContextMenu"
>
<button
class="flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
type="button"
:class="{ invisible: item.type !== 'TABLE' }"
@click.stop="toggleTableSelection(item)"
>
<button class="flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground" type="button" :class="{ invisible: item.type !== 'TABLE' }" @click.stop="toggleTableSelection(item)">
<CheckSquare v-if="selectedTableIds.has(item.id)" class="h-3.5 w-3.5 text-primary" />
<Square v-else class="h-3.5 w-3.5" />
</button>
@@ -1516,26 +1332,15 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<span v-else class="h-5 w-5 shrink-0" :class="{ 'ml-4': item.partitionParentId }" />
<component :is="iconFor(item)" class="h-3.5 w-3.5 shrink-0" :class="iconClass(item.type)" />
<span class="truncate text-[13px] font-medium text-foreground">{{ item.name }}</span>
<span
v-if="item.partitionCount"
class="shrink-0 rounded border bg-muted/40 px-1.5 py-0.5 text-[10px] font-medium leading-none text-muted-foreground"
>
<span v-if="item.partitionCount" class="shrink-0 rounded border bg-muted/40 px-1.5 py-0.5 text-[10px] font-medium leading-none text-muted-foreground">
{{ t("objects.partitions", { count: item.partitionCount }) }}
</span>
</div>
<div class="truncate text-xs text-muted-foreground">{{ typeLabel(item.type) }}</div>
<div
v-if="hasCreatedAt"
class="truncate text-xs tabular-nums text-muted-foreground"
:title="formatObjectBrowserTimestamp(item.created_at)"
>
<div v-if="hasCreatedAt" class="truncate text-xs tabular-nums text-muted-foreground" :title="formatObjectBrowserTimestamp(item.created_at)">
{{ formatObjectBrowserTimestamp(item.created_at) }}
</div>
<div
v-if="hasUpdatedAt"
class="truncate text-xs tabular-nums text-muted-foreground"
:title="formatObjectBrowserTimestamp(item.updated_at)"
>
<div v-if="hasUpdatedAt" class="truncate text-xs tabular-nums text-muted-foreground" :title="formatObjectBrowserTimestamp(item.updated_at)">
{{ formatObjectBrowserTimestamp(item.updated_at) }}
</div>
<div v-if="hasComments" class="truncate text-xs text-muted-foreground" :title="item.comment || ''">
@@ -1549,45 +1354,17 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<div class="flex h-8 shrink-0 items-center gap-2 border-b bg-muted/20 px-3">
<Code2 class="h-3.5 w-3.5 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate text-xs font-medium">{{ sourceTitle(sourceRow) }}</span>
<Button
v-if="sourceEditing"
variant="ghost"
size="sm"
class="h-6 px-2 text-xs"
:disabled="sourceSaving || !sourceDraft.trim()"
@click="saveSource"
>
<Button v-if="sourceEditing" variant="ghost" size="sm" class="h-6 px-2 text-xs" :disabled="sourceSaving || !sourceDraft.trim()" @click="saveSource">
<Loader2 v-if="sourceSaving" class="mr-1 h-3 w-3 animate-spin" />
{{ t("objects.saveSource") }}
</Button>
<Button
v-if="sourceEditing"
variant="ghost"
size="sm"
class="h-6 px-2 text-xs"
:disabled="sourceSaving"
@click="cancelEditSource"
>
<Button v-if="sourceEditing" variant="ghost" size="sm" class="h-6 px-2 text-xs" :disabled="sourceSaving" @click="cancelEditSource">
{{ t("objects.cancelEdit") }}
</Button>
<Button
v-if="!sourceEditing"
variant="ghost"
size="icon"
class="h-5 w-5"
:disabled="!sourceContent"
@click="copySource"
>
<Button v-if="!sourceEditing" variant="ghost" size="icon" class="h-5 w-5" :disabled="!sourceContent" @click="copySource">
<Copy class="h-3 w-3" />
</Button>
<Button
v-if="!sourceEditing"
variant="ghost"
size="icon"
class="h-5 w-5"
:disabled="!sourceContent"
@click="editSource"
>
<Button v-if="!sourceEditing" variant="ghost" size="icon" class="h-5 w-5" :disabled="!sourceContent" @click="editSource">
<PencilLine class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="closeSource">
@@ -1601,18 +1378,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
{{ sourceError }}
</div>
<div v-else-if="sourceEditing" class="flex min-h-0 flex-1 flex-col" data-object-source-editor>
<QueryEditor
v-model="sourceDraft"
class="min-h-0 flex-1"
:connection-id="props.connection.id"
:database="props.database"
:schema="selectedSchema"
:database-type="props.connection.db_type"
:dialect="sourceDialect"
:format-dialect="sourceFormatDialect"
force-word-wrap
@save="saveSource"
/>
<QueryEditor v-model="sourceDraft" class="min-h-0 flex-1" :connection-id="props.connection.id" :database="props.database" :schema="selectedSchema" :database-type="props.connection.db_type" :dialect="sourceDialect" :format-dialect="sourceFormatDialect" force-word-wrap @save="saveSource" />
<div v-if="sourceSaveError" class="shrink-0 border-t px-3 py-2 text-xs text-destructive">
{{ sourceSaveError }}
</div>
@@ -1636,22 +1402,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
</div>
</div>
<DangerConfirmDialog
v-model:open="showDropConfirm"
:title="dropConfirmTitle()"
:details="dropConfirmMessage()"
:confirm-label="t('dangerDialog.deleteConfirm')"
@confirm="confirmDrop"
/>
<DangerConfirmDialog v-model:open="showDropConfirm" :title="dropConfirmTitle()" :details="dropConfirmMessage()" :confirm-label="t('dangerDialog.deleteConfirm')" @confirm="confirmDrop" />
<DangerConfirmDialog
v-model:open="showBatchDropConfirm"
:title="t('objects.confirmBatchDropTitle')"
:message="t('objects.confirmBatchDropMessage', { count: selectedTableCount })"
:sql="batchDropPreviewSql"
:confirm-label="t('objects.dropSelected')"
@confirm="confirmBatchDropTables"
/>
<DangerConfirmDialog v-model:open="showBatchDropConfirm" :title="t('objects.confirmBatchDropTitle')" :message="t('objects.confirmBatchDropMessage', { count: selectedTableCount })" :sql="batchDropPreviewSql" :confirm-label="t('objects.dropSelected')" @confirm="confirmBatchDropTables" />
<Dialog v-model:open="showRenameDialog">
<DialogContent class="sm:max-w-[420px]">
@@ -1659,16 +1412,8 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<DialogTitle>{{ t("contextMenu.renameObjectTitle") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3">
<Input
v-model="renameInput"
:placeholder="t('contextMenu.renameObjectNamePlaceholder')"
@keydown.enter.prevent="confirmRename"
/>
<pre
v-if="renamePreviewSqlText"
class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap"
v-html="highlight(renamePreviewSqlText)"
></pre>
<Input v-model="renameInput" :placeholder="t('contextMenu.renameObjectNamePlaceholder')" @keydown.enter.prevent="confirmRename" />
<pre v-if="renamePreviewSqlText" class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap" v-html="highlight(renamePreviewSqlText)"></pre>
<p v-if="renameError" class="text-sm text-destructive">{{ renameError }}</p>
</div>
<DialogFooter>
@@ -1689,14 +1434,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
@confirm="confirmTruncateTable"
/>
<DangerConfirmDialog
v-model:open="showEmptyConfirm"
:title="t('contextMenu.confirmEmptyTableTitle')"
:message="t('contextMenu.confirmEmptyTableMessage', { name: emptyTarget?.name ?? '' })"
:sql="emptyPreviewSql"
:confirm-label="t('contextMenu.emptyTable')"
@confirm="confirmEmptyTable"
/>
<DangerConfirmDialog v-model:open="showEmptyConfirm" :title="t('contextMenu.confirmEmptyTableTitle')" :message="t('contextMenu.confirmEmptyTableMessage', { name: emptyTarget?.name ?? '' })" :sql="emptyPreviewSql" :confirm-label="t('contextMenu.emptyTable')" @confirm="confirmEmptyTable" />
<ProcedureExecutionDialog
v-if="procedureExecutionTarget"
@@ -1715,11 +1453,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<DialogHeader>
<DialogTitle>{{ t("contextMenu.duplicateNameTitle") }}</DialogTitle>
</DialogHeader>
<Input
v-model="duplicateTableName"
:placeholder="t('contextMenu.duplicateNamePlaceholder')"
@keydown.enter.prevent="confirmDuplicateStructure"
/>
<Input v-model="duplicateTableName" :placeholder="t('contextMenu.duplicateNamePlaceholder')" @keydown.enter.prevent="confirmDuplicateStructure" />
<DialogFooter>
<Button variant="outline" @click="showDuplicateDialog = false">{{ t("dangerDialog.cancel") }}</Button>
<Button :disabled="!duplicateTableName.trim()" @click="confirmDuplicateStructure">
@@ -8,12 +8,7 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
import { Input } from "@/components/ui/input";
import LightTooltip from "@/components/ui/LightTooltip.vue";
import { loadRoutineParameters } from "@/lib/routineParameters";
import {
acceptsRoutineInput,
buildProcedureExecutionSql,
buildProcedureExecutionSqlFromValues,
type RoutineParameterValue,
} from "@/lib/routineExecutionSql";
import { acceptsRoutineInput, buildProcedureExecutionSql, buildProcedureExecutionSqlFromValues, type RoutineParameterValue } from "@/lib/routineExecutionSql";
import type { DatabaseType } from "@/types/database";
const { t } = useI18n();
@@ -143,9 +138,7 @@ function canEditParameter(parameter: RoutineParameterValue): boolean {
<template>
<Dialog v-model:open="open">
<DialogContent
class="max-h-[86vh] border border-border !bg-background text-foreground shadow-2xl !backdrop-blur-none sm:max-w-[780px]"
>
<DialogContent class="max-h-[86vh] border border-border !bg-background text-foreground shadow-2xl !backdrop-blur-none sm:max-w-[780px]">
<DialogHeader>
<DialogTitle>{{ t("contextMenu.confirmExecuteProcedureTitle") }}</DialogTitle>
</DialogHeader>
@@ -170,19 +163,14 @@ function canEditParameter(parameter: RoutineParameterValue): boolean {
</div>
</div>
<div
v-if="loading"
class="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2 text-sm text-muted-foreground"
>
<div v-if="loading" class="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("contextMenu.loadingProcedureParameters") }}
</div>
<div v-else-if="parameters.length" class="overflow-x-auto rounded-md border bg-background">
<div class="min-w-[650px]">
<div
class="grid grid-cols-[minmax(120px,1.2fr)_minmax(96px,1fr)_72px_minmax(160px,1.5fr)_64px_86px] border-b bg-muted px-3 py-2 text-xs font-medium text-muted-foreground"
>
<div class="grid grid-cols-[minmax(120px,1.2fr)_minmax(96px,1fr)_72px_minmax(160px,1.5fr)_64px_86px] border-b bg-muted px-3 py-2 text-xs font-medium text-muted-foreground">
<div>{{ t("contextMenu.parameterName") }}</div>
<div>{{ t("contextMenu.parameterType") }}</div>
<div>{{ t("contextMenu.parameterMode") }}</div>
@@ -191,54 +179,24 @@ function canEditParameter(parameter: RoutineParameterValue): boolean {
<div class="flex items-center gap-1">
{{ t("contextMenu.parameterDefault") }}
<LightTooltip :text="t('contextMenu.parameterDefaultHint')" side="top" :delay="150">
<button
type="button"
class="inline-flex h-4 w-4 items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground"
:aria-label="t('contextMenu.parameterDefaultHint')"
>
<button type="button" class="inline-flex h-4 w-4 items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground" :aria-label="t('contextMenu.parameterDefaultHint')">
<CircleHelp class="h-3.5 w-3.5" />
</button>
</LightTooltip>
</div>
</div>
<div
v-for="parameter in parameters"
:key="`${parameter.ordinal}:${parameter.name}`"
class="grid grid-cols-[minmax(120px,1.2fr)_minmax(96px,1fr)_72px_minmax(160px,1.5fr)_64px_86px] items-center gap-2 border-b px-3 py-2 text-sm last:border-b-0"
>
<div v-for="parameter in parameters" :key="`${parameter.ordinal}:${parameter.name}`" class="grid grid-cols-[minmax(120px,1.2fr)_minmax(96px,1fr)_72px_minmax(160px,1.5fr)_64px_86px] items-center gap-2 border-b px-3 py-2 text-sm last:border-b-0">
<div class="min-w-0 truncate font-medium">{{ parameter.name }}</div>
<div class="min-w-0 truncate text-muted-foreground">{{ parameter.dataType || "-" }}</div>
<div class="text-xs text-muted-foreground">{{ parameter.mode }}</div>
<Input
v-model="parameter.value"
class="h-8 bg-background font-mono text-xs"
:disabled="!canEditParameter(parameter) || parameter.useNull || parameter.useDefault"
:placeholder="
canEditParameter(parameter) ? t('contextMenu.parameterValuePlaceholder') : t('contextMenu.outputOnly')
"
/>
<input
type="checkbox"
class="h-4 w-4 accent-primary"
:checked="!!parameter.useNull"
:disabled="!canEditParameter(parameter) || parameter.useDefault"
@change="(event: Event) => (parameter.useNull = (event.target as HTMLInputElement).checked)"
/>
<input
type="checkbox"
class="h-4 w-4 accent-primary"
:checked="!!parameter.useDefault"
:disabled="!canEditParameter(parameter) || !parameter.hasDefault || parameter.useNull"
@change="(event: Event) => (parameter.useDefault = (event.target as HTMLInputElement).checked)"
/>
<Input v-model="parameter.value" class="h-8 bg-background font-mono text-xs" :disabled="!canEditParameter(parameter) || parameter.useNull || parameter.useDefault" :placeholder="canEditParameter(parameter) ? t('contextMenu.parameterValuePlaceholder') : t('contextMenu.outputOnly')" />
<input type="checkbox" class="h-4 w-4 accent-primary" :checked="!!parameter.useNull" :disabled="!canEditParameter(parameter) || parameter.useDefault" @change="(event: Event) => (parameter.useNull = (event.target as HTMLInputElement).checked)" />
<input type="checkbox" class="h-4 w-4 accent-primary" :checked="!!parameter.useDefault" :disabled="!canEditParameter(parameter) || !parameter.hasDefault || parameter.useNull" @change="(event: Event) => (parameter.useDefault = (event.target as HTMLInputElement).checked)" />
</div>
</div>
</div>
<p
v-else-if="loadError"
class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800"
>
<p v-else-if="loadError" class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
{{ t("contextMenu.procedureParametersUnavailable") }}
</p>
@@ -253,12 +211,7 @@ function canEditParameter(parameter: RoutineParameterValue): boolean {
{{ t("contextMenu.resetSqlPreview") }}
</Button>
</div>
<textarea
:value="sqlDraft"
class="min-h-28 w-full resize-y rounded-md border border-input bg-background px-3 py-2 font-mono text-xs outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/40"
spellcheck="false"
@input="onSqlInput"
></textarea>
<textarea :value="sqlDraft" class="min-h-28 w-full resize-y rounded-md border border-input bg-background px-3 py-2 font-mono text-xs outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/40" spellcheck="false" @input="onSqlInput"></textarea>
</div>
</div>