diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index c5858fbb5..be3802757 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -85,6 +85,7 @@ import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing"; import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit"; import { getCachedTableMetadata, loadTableMetadata, TABLE_METADATA_CACHE_TTL_MS, tableMetadataToDataTabMeta } from "@/lib/metadata/tableMetadataCache"; import { + canConfigureVisibleSchemasForTreeNode, canCreateConnectionNamespace, canCreateDatabaseNodeNamespace, canEditDatabaseProperties as canEditDatabasePropertiesForNode, @@ -161,7 +162,6 @@ import { isSqlServerLinkedNode } from "@/lib/database/sqlServerLinkedServers"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue"; import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue"; -import { isSchemaAware } from "@/lib/database/databaseFeatureSupport"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import LightTooltip from "@/components/ui/LightTooltip.vue"; @@ -4251,15 +4251,9 @@ const canConfigureVisibleDatabases = computed(() => { }); const canConfigureVisibleSchemas = computed(() => { - if (props.node.type === "database" && props.node.connectionId && props.node.database != null) { - const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type; - return isSchemaAware(dbType); - } - if (props.node.type === "connection" && props.node.connectionId) { - const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type; - return isSchemaAware(dbType) && !usesTreeSchemaMode(dbType); - } - return false; + if (!props.node.connectionId) return false; + const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type; + return canConfigureVisibleSchemasForTreeNode(dbType, props.node.type, props.node.database); }); const canCopyFinalProxyPort = computed(() => { diff --git a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue index d90bf4de9..d31221f97 100644 --- a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue +++ b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue @@ -67,6 +67,8 @@ watch( if (!open) return; loadDatabases().catch(() => {}); }, + // Tree-level async hosts mount the dialog with `open` already true. + { immediate: true }, ); watch(showSystemDatabases, (show) => { diff --git a/apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue b/apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue index 367ee370e..b6efe71b5 100644 --- a/apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue +++ b/apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue @@ -64,6 +64,8 @@ watch( loadSchemas().catch(() => {}); } }, + // Tree-level async hosts mount the dialog with `open` already true. + { immediate: true }, ); function initDraftMode() { diff --git a/apps/desktop/src/lib/database/databaseFeatureSupport.ts b/apps/desktop/src/lib/database/databaseFeatureSupport.ts index 758952b1b..51c32eb66 100644 --- a/apps/desktop/src/lib/database/databaseFeatureSupport.ts +++ b/apps/desktop/src/lib/database/databaseFeatureSupport.ts @@ -40,6 +40,12 @@ export function usesTreeSchemaMode(dbType?: DatabaseType): boolean { return !!dbType && TREE_SCHEMA_TYPES.has(dbType); } +export function canConfigureVisibleSchemasForTreeNode(dbType: DatabaseType | undefined, nodeType: TreeNodeType, database?: string | null): boolean { + if (!isSchemaAware(dbType)) return false; + if (nodeType === "database") return database != null; + return nodeType === "connection" && !usesTreeSchemaMode(dbType); +} + export function usesDatabaseObjectTreeMode(dbType?: DatabaseType): boolean { return !!dbType && DATABASE_OBJECT_TREE_TYPES.has(dbType); } diff --git a/packages/app-tests/connectionVisibleDatabases.test.ts b/packages/app-tests/connectionVisibleDatabases.test.ts index 5648f249a..211c732b6 100644 --- a/packages/app-tests/connectionVisibleDatabases.test.ts +++ b/packages/app-tests/connectionVisibleDatabases.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { test } from "vitest"; import { appendVisibleDatabaseSelection, buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, visibleDatabaseSelectionIsStale, initialVisibleDatabaseSelection } from "../../apps/desktop/src/lib/connection/connectionVisibleDatabases.ts"; -import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterDatabaseNamesForVisiblePicker } from "../../apps/desktop/src/lib/database/visibleDatabases.ts"; +import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForConnection, normalizeVisibleSchemaSelection } from "../../apps/desktop/src/lib/database/visibleDatabases.ts"; import type { ConnectionConfig } from "../../apps/desktop/src/types/database.ts"; function config(overrides: Partial = {}): ConnectionConfig { @@ -95,6 +95,14 @@ test("OceanBase Oracle uses schema filtering for visible object selection", () = assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "mysql", driver_profile: "oceanbase" })), false); }); +test("Vastbase schema filters preserve ordinary schemas and explicit empty selections", () => { + const schemas = ["public", "app"]; + assert.deepEqual(filterSchemaNamesForConnection(schemas, config({ db_type: "vastbase", database: "vastbase" }), "vastbase"), schemas); + assert.deepEqual(filterSchemaNamesForConnection(schemas, config({ db_type: "vastbase", database: "vastbase", visible_schemas: { vastbase: [] } }), "vastbase"), []); + assert.deepEqual(normalizeVisibleSchemaSelection([], schemas), []); + assert.deepEqual(normalizeVisibleSchemaSelection(["app", "missing", "app", "public"], schemas), ["app", "public"]); +}); + test("Dameng default SYSDBA user remains selectable", () => { assert.deepEqual(filterDatabaseNamesForConnection(["SYS", "SYSDBA", "SYSAUDITOR"], config({ db_type: "dameng" })), ["SYSDBA"]); }); diff --git a/packages/app-tests/databaseCapabilities.test.ts b/packages/app-tests/databaseCapabilities.test.ts index 44b7c67d6..a9b429bdd 100644 --- a/packages/app-tests/databaseCapabilities.test.ts +++ b/packages/app-tests/databaseCapabilities.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { test } from "vitest"; -import { isSchemaAware, supportsClearableQuerySchema, supportsDatabaseCreation, usesTreeSchemaMode } from "../../apps/desktop/src/lib/database/databaseCapabilities.ts"; +import { canConfigureVisibleSchemasForTreeNode, isSchemaAware, supportsClearableQuerySchema, supportsDatabaseCreation, usesTreeSchemaMode } from "../../apps/desktop/src/lib/database/databaseCapabilities.ts"; test("TDengine uses database/catalog tree nodes without a schema layer", () => { assert.equal(isSchemaAware("tdengine"), false); @@ -17,6 +17,22 @@ test("GoldenDB and Vastbase expose database creation", () => { assert.equal(supportsDatabaseCreation("vastbase"), true); }); +test("Vastbase keeps PostgreSQL-like schema filtering on database nodes", () => { + assert.equal(isSchemaAware("vastbase"), true); + assert.equal(usesTreeSchemaMode("vastbase"), true); + assert.equal(canConfigureVisibleSchemasForTreeNode("vastbase", "database", "vastbase"), true); + assert.equal(canConfigureVisibleSchemasForTreeNode("vastbase", "connection"), false); +}); + +test("visible schema menu capability preserves adjacent database families", () => { + assert.equal(canConfigureVisibleSchemasForTreeNode("oracle", "connection"), true); + assert.equal(canConfigureVisibleSchemasForTreeNode("dameng", "connection"), true); + assert.equal(canConfigureVisibleSchemasForTreeNode("oceanbase-oracle", "connection"), false); + assert.equal(canConfigureVisibleSchemasForTreeNode("postgres", "database", "app"), true); + assert.equal(canConfigureVisibleSchemasForTreeNode("sqlserver", "database", "app"), true); + assert.equal(canConfigureVisibleSchemasForTreeNode("mysql", "database", "app"), false); +}); + test("only Oracle-compatible single-database schemas can be cleared from query tabs", () => { assert.equal(supportsClearableQuerySchema("oracle"), true); assert.equal(supportsClearableQuerySchema("dameng"), true); diff --git a/packages/app-tests/sidebarDialogRouting.test.ts b/packages/app-tests/sidebarDialogRouting.test.ts index 292524ae7..7d76eefd0 100644 --- a/packages/app-tests/sidebarDialogRouting.test.ts +++ b/packages/app-tests/sidebarDialogRouting.test.ts @@ -6,6 +6,8 @@ const connectionTree = readFileSync("apps/desktop/src/components/sidebar/Connect const treeItem = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8"); const dialogHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue", "utf8"); const dialogState = readFileSync("apps/desktop/src/components/sidebar/sidebarTreeDialogState.ts", "utf8"); +const visibleDatabasesDialog = readFileSync("apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue", "utf8"); +const visibleSchemasDialog = readFileSync("apps/desktop/src/components/sidebar/VisibleSchemasDialog.vue", "utf8"); function occurrences(source: string, value: string): number { return source.split(value).length - 1; @@ -43,6 +45,11 @@ test("saved object dialogs refresh the immutable active target", () => { assert.match(connectionTree, /findSidebarActionTarget\(store\.treeNodes, target\)/); }); +test("lazy visible object dialogs load when first mounted open", () => { + assert.match(visibleDatabasesDialog, /watch\([\s\S]*?loadDatabases\(\)[\s\S]*?\{ immediate: true \}/); + assert.match(visibleSchemasDialog, /watch\([\s\S]*?loadSchemas\(\)[\s\S]*?\{ immediate: true \}/); +}); + test("shared dialog state is owner-gated and confirm handlers use snapshots", () => { assert.match(dialogState, /export const sidebarTreeDialogOwner = shallowRef\(null\)/); assert.match(dialogState, /export const sidebarDangerTarget = shallowRef\(null\)/);