fix(docs): move Documentation to database context menu

This commit is contained in:
Fernando Possebon 2026-08-07 23:37:50 -03:00 committed by GitHub
parent 9a255aa135
commit bdf87ac255
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 560 additions and 24 deletions

View File

@ -6,7 +6,6 @@ import {
ArrowDown,
ArrowRightLeft,
ArrowUp,
BookOpen,
Braces,
CheckSquare,
Clipboard,
@ -1463,16 +1462,6 @@ function openDiagram(row: ObjectBrowserRow) {
};
}
function openDocs(row: ObjectBrowserRow) {
// The docs viewer documents the whole schema rather than one object, so the
// row only supplies which schema to collect.
connectionStore.docsSource = {
connectionId: props.connection.id,
database: props.database,
schema: row.schema || selectedSchema.value,
};
}
function openTableImport(row: ObjectBrowserRow) {
if (row.type !== "TABLE") return;
connectionStore.tableImportSource = {
@ -2663,12 +2652,7 @@ function getTableMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
...(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 },
{ label: t("docs.title"), action: () => openDocs(item), icon: BookOpen },
]
: []),
...(canOpenDiagram.value ? [{ label: t("diagram.open"), action: () => openDiagram(item), icon: Network }] : []),
...(canOpenTableImport.value ? [{ label: t("contextMenu.importData"), action: () => openTableImport(item), icon: Download }] : []),
{ label: t("dataCompare.title"), action: () => openDataCompare(item), icon: ArrowRightLeft },
{ label: "", separator: true },
@ -2718,12 +2702,7 @@ function getViewMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
},
...(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: t("docs.title"), action: () => openDocs(item), icon: BookOpen },
]
: []),
...(canOpenDiagram.value ? [{ label: t("diagram.open"), action: () => openDiagram(item), icon: Network }] : []),
{ label: "", separator: true },
exportDataSubmenu(item),
{ label: t("contextMenu.exportDatabase"), action: () => openDatabaseExport(item), icon: Upload },

View File

@ -0,0 +1,38 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
function source(relative: string): string {
return readFileSync(fileURLToPath(new URL(relative, import.meta.url)), "utf8");
}
/**
* Slices the body of one sidebar menu builder out of SidebarTreeRuntimeHost.vue.
* Matching the builder rather than a formatted line keeps this guard indifferent
* to how oxfmt lays the menu pushes out.
*/
function sidebarMenuBuilder(name: string): string {
const text = source("../../sidebar/SidebarTreeRuntimeHost.vue");
const start = text.indexOf(`function ${name}(`);
expect(start, `${name} not found`).toBeGreaterThan(-1);
const next = text.indexOf("\nfunction ", start + 1);
return text.slice(start, next === -1 ? undefined : next);
}
describe("documentation menu placement", () => {
it("registers the entry on database and schema nodes", () => {
// The docs viewer documents a database or a schema, so it belongs on the
// tree nodes that name one. buildDatabaseSidebarMenu serves both types.
expect(sidebarMenuBuilder("buildDatabaseSidebarMenu")).toContain("docs.title");
});
it("keeps the entry off the sidebar's per-object menu", () => {
expect(sidebarMenuBuilder("buildObjectSidebarMenu")).not.toContain("docs.title");
});
it("keeps the entry off the object browser's menus", () => {
// openDocs never read the row's table: a table-level entry advertised a
// scope the viewer cannot render.
expect(source("../ObjectBrowser.vue")).not.toContain("docs.title");
});
});

View File

@ -11,6 +11,7 @@ import { useSidebarTreeToolRuntime } from "@/composables/useSidebarTreeToolRunti
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import {
BookOpen,
Database,
ChevronsDown,
FolderOpen,
@ -315,7 +316,7 @@ const { copyStructureAs, copyStructureDocText, copyStructurePreview, exportData,
acceptedSelectionIds: () => acceptedSelectionIds,
});
const { openAllDatabasesExport, openDataCompare, openDatabaseExport, openDatabaseSearch, openDiagram, openFieldLineage, openScheduledBackups, openSchemaDiff, openSqlFileExecution, openStructureEditor, openTableImport, openTransfer } = useSidebarTreeToolRuntime({
const { openAllDatabasesExport, openDataCompare, openDatabaseExport, openDatabaseSearch, openDiagram, openDocs, openFieldLineage, openScheduledBackups, openSchemaDiff, openSqlFileExecution, openStructureEditor, openTableImport, openTransfer } = useSidebarTreeToolRuntime({
activeNode,
connectionStore,
queryStore,
@ -4203,6 +4204,7 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
}
if (canOpenDiagram.value) {
items.push({ label: t("diagram.open"), action: openDiagram, icon: Network });
items.push({ label: t("docs.title"), action: openDocs, icon: BookOpen });
}
if (canOpenDatabaseSearch.value) {
items.push({ label: t("databaseSearch.open"), action: openDatabaseSearch, icon: Search });

View File

@ -0,0 +1,45 @@
import { shallowRef } from "vue";
import { describe, expect, it } from "vitest";
import type { TreeNode } from "@/types/database";
import { useSidebarTreeToolRuntime } from "@/composables/useSidebarTreeToolRuntime";
function setup(node: Partial<TreeNode>) {
const activeNode = shallowRef({ id: "n-1", label: "node", children: [], ...node } as TreeNode);
const connectionStore = { docsSource: null as unknown };
const runtime = useSidebarTreeToolRuntime({
activeNode,
connectionStore: connectionStore as never,
queryStore: {} as never,
settingsStore: {} as never,
tableChildObjectName: () => "",
});
return { connectionStore, runtime };
}
describe("useSidebarTreeToolRuntime openDocs", () => {
it("documents the whole database when invoked on a database node", () => {
const { connectionStore, runtime } = setup({ type: "database", label: "shop", connectionId: "conn-1", database: "shop" });
runtime.openDocs();
// An absent schema is what makes the collector document every schema.
expect(connectionStore.docsSource).toEqual({ connectionId: "conn-1", database: "shop", schema: undefined });
});
it("narrows to a single schema when invoked on a schema node", () => {
const { connectionStore, runtime } = setup({ type: "schema", label: "public", connectionId: "conn-1", database: "shop", schema: "public" });
runtime.openDocs();
expect(connectionStore.docsSource).toEqual({ connectionId: "conn-1", database: "shop", schema: "public" });
});
it("does nothing when the node carries no database", () => {
const { connectionStore, runtime } = setup({ type: "connection", label: "local", connectionId: "conn-1" });
runtime.openDocs();
expect(connectionStore.docsSource).toBeNull();
});
});

View File

@ -63,6 +63,18 @@ export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions
};
}
function openDocs() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
connectionStore.docsSource = {
connectionId: node.connectionId,
database: node.database,
// A database node has no schema, and an absent schema is what tells the
// collector to document every schema in the database.
schema: node.schema,
};
}
function openDatabaseSearch() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
@ -135,6 +147,7 @@ export function useSidebarTreeToolRuntime(options: SidebarTreeToolRuntimeOptions
openDatabaseExport,
openDatabaseSearch,
openDiagram,
openDocs,
openFieldLineage,
openScheduledBackups,
openSchemaDiff,

View File

@ -0,0 +1,309 @@
# Documentation Menu at Database Scope Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Move the "Documentation" context-menu entry out of the object browser's table and view menus and into the sidebar's database and schema context menu, so the entry sits at the scope it actually documents.
**Architecture:** A new `openDocs` action in `useSidebarTreeToolRuntime` writes `connectionStore.docsSource` from the active tree node, exactly as the neighbouring `openDiagram` does. One `items.push` in `buildDatabaseSidebarMenu` exposes it on database and schema nodes. The two `ObjectBrowser.vue` entries and their helper are deleted. No backend, dialog, or viewer code changes.
**Tech Stack:** Vue 3 SFC with `<script setup>`, TypeScript, Pinia (`connectionStore`), vue-i18n, `@lucide/vue` icons, Vitest.
## Global Constraints
- Branch is `fix/docs-menu-database-scope`, cut from `upstream/main` at `29fe57143`. Work on it directly; do not create a worktree.
- Do not modify `VERSION`, `package.json`, or any file under `crates/`. Version numbers belong to upstream's `scripts/release.mjs`.
- Do not add an i18n key. `docs.title` already resolves to "Documentation" in all eight locales, added by PR #5559.
- Icons come from `@lucide/vue`, not `lucide-vue-next`.
- Do not touch either `t("diagram.open")` menu entry.
- Reuse the existing `canOpenDiagram` computed as the visibility gate. Do not add a `docs` key to `DATABASE_PRODUCT_CAPABILITY_KEYS`; that is deliberately out of scope and flagged as a follow-up in the PR description.
---
## File Structure
| File | Responsibility | Change |
|---|---|---|
| `apps/desktop/src/composables/useSidebarTreeToolRuntime.ts` | Sidebar actions that open a tool dialog by writing a `*Source` field on `connectionStore`. | Add `openDocs`; export it. |
| `apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts` | Behavioural cover for `openDocs`. | Create. |
| `apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue` | Builds every sidebar context menu. | Import `BookOpen`, destructure `openDocs`, push one menu item in `buildDatabaseSidebarMenu`. |
| `apps/desktop/src/components/objects/ObjectBrowser.vue` | Object list panel and its per-object context menus. | Delete the docs entries, the `openDocs` helper, and the orphaned `BookOpen` import. |
| `apps/desktop/src/components/objects/__tests__/docsMenuPlacement.spec.ts` | Guards the placement so the entry cannot drift back onto table menus. | Create. |
---
## Task 1: Sidebar `openDocs` action
**Files:**
- Modify: `apps/desktop/src/composables/useSidebarTreeToolRuntime.ts:55-64` (insert after `openDiagram`), and the return block at `:132-145`
- Test: `apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts` (create)
**Interfaces:**
- Consumes: `options.activeNode: ShallowRef<TreeNode>` and `options.connectionStore`, both already destructured at the top of `useSidebarTreeToolRuntime`.
- Produces: `openDocs(): void`, returned from `useSidebarTreeToolRuntime`. It sets `connectionStore.docsSource` to `{ connectionId: string; database: string; schema: string | undefined }`. Task 2 binds this to a menu item.
**Why `schema` is passed through untouched:** `DatabaseDocsDialog.vue:93` reads `props.prefillSchema ? [props.prefillSchema] : []`. An `undefined` schema therefore sends an empty schema list, which the collector treats as "everything it finds" — the whole database. A database tree node has no `schema`, a schema node does, so forwarding the field verbatim gives both scopes for free.
**Why no `tableName`:** `openDiagram` passes one because the diagram dialog has a `tableName` prop. `DatabaseDocsDialog` has only `prefillConnectionId`, `prefillDatabase`, and `prefillSchema`. Passing `tableName` would be dead data.
- [ ] **Step 1: Write the failing test**
Create `apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts`.
The composable imports only types plus two pure helpers from `@/lib/sidebar/sidebarExportRuntime`, so no `vi.mock` is needed. `queryStore`, `settingsStore`, and `tableChildObjectName` are unused by `openDocs`; they are passed as minimal stubs because the options object requires them.
```ts
import { shallowRef } from "vue";
import { describe, expect, it } from "vitest";
import type { TreeNode } from "@/types/database";
import { useSidebarTreeToolRuntime } from "@/composables/useSidebarTreeToolRuntime";
function setup(node: Partial<TreeNode>) {
const activeNode = shallowRef({ id: "n-1", label: "node", children: [], ...node } as TreeNode);
const connectionStore = { docsSource: null as unknown };
const runtime = useSidebarTreeToolRuntime({
activeNode,
connectionStore: connectionStore as never,
queryStore: {} as never,
settingsStore: {} as never,
tableChildObjectName: () => "",
});
return { connectionStore, runtime };
}
describe("useSidebarTreeToolRuntime openDocs", () => {
it("documents the whole database when invoked on a database node", () => {
const { connectionStore, runtime } = setup({ type: "database", label: "shop", connectionId: "conn-1", database: "shop" });
runtime.openDocs();
// An absent schema is what makes the collector document every schema.
expect(connectionStore.docsSource).toEqual({ connectionId: "conn-1", database: "shop", schema: undefined });
});
it("narrows to a single schema when invoked on a schema node", () => {
const { connectionStore, runtime } = setup({ type: "schema", label: "public", connectionId: "conn-1", database: "shop", schema: "public" });
runtime.openDocs();
expect(connectionStore.docsSource).toEqual({ connectionId: "conn-1", database: "shop", schema: "public" });
});
it("does nothing when the node carries no database", () => {
const { connectionStore, runtime } = setup({ type: "connection", label: "local", connectionId: "conn-1" });
runtime.openDocs();
expect(connectionStore.docsSource).toBeNull();
});
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `npx vitest run apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts`
Expected: FAIL. All three cases error with `runtime.openDocs is not a function`.
- [ ] **Step 3: Write the implementation**
In `apps/desktop/src/composables/useSidebarTreeToolRuntime.ts`, insert immediately after the closing brace of `openDiagram` (currently line 64):
```ts
function openDocs() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
connectionStore.docsSource = {
connectionId: node.connectionId,
database: node.database,
// A database node has no schema, and an absent schema is what tells the
// collector to document every schema in the database.
schema: node.schema,
};
}
```
Then add `openDocs,` to the returned object, keeping the existing alphabetical order — between `openDiagram,` and `openFieldLineage,`:
```ts
return {
openAllDatabasesExport,
openDataCompare,
openDatabaseExport,
openDatabaseSearch,
openDiagram,
openDocs,
openFieldLineage,
openScheduledBackups,
openSchemaDiff,
openSqlFileExecution,
openStructureEditor,
openTableImport,
openTransfer,
};
```
- [ ] **Step 4: Run the test to verify it passes**
Run: `npx vitest run apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts`
Expected: PASS, 3 tests.
- [ ] **Step 5: Commit**
```bash
git add apps/desktop/src/composables/useSidebarTreeToolRuntime.ts apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts
git commit -m "feat(docs): add a sidebar action opening documentation for a node"
```
---
## Task 2: Move the menu entry
**Files:**
- Modify: `apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue:13` (icon import), `:312` (destructure), `:4204-4206` (menu push)
- Modify: `apps/desktop/src/components/objects/ObjectBrowser.vue:9` (icon import), `:1466-1474` (helper), `:2666-2671` and `:2721-2726` (menu entries)
- Test: `apps/desktop/src/components/objects/__tests__/docsMenuPlacement.spec.ts` (create)
**Interfaces:**
- Consumes: `openDocs()` from Task 1, reached through the existing `useSidebarTreeToolRuntime(...)` destructure at `SidebarTreeRuntimeHost.vue:312`.
- Produces: no new exports.
**Why the single push covers both node types:** line 4204 sits inside `buildDatabaseSidebarMenu` (lines 4147-4259), whose outer guard is `node.type === "database" || node.type === "schema"`. The other `t("diagram.open")` push, at line 4483, is in `buildObjectSidebarMenu` and must be left alone.
**On the placement test:** this repository contains both mounted component specs and source-text specs. A mounted test is impractical for a 4700-line SFC whose menus are assembled from dozens of store-backed computeds, and the behaviour that matters — what `openDocs` writes — is already covered by Task 1. The spec below is a deliberately narrow placement guard: it asserts which file registers `docs.title`, nothing about rendering. Do not extend it into a substitute for behavioural cover.
- [ ] **Step 1: Write the failing test**
Create `apps/desktop/src/components/objects/__tests__/docsMenuPlacement.spec.ts`:
```ts
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
function source(relative: string): string {
return readFileSync(fileURLToPath(new URL(relative, import.meta.url)), "utf8");
}
describe("documentation menu placement", () => {
it("registers the entry in the sidebar database menu", () => {
// The docs viewer documents a database or a schema, so it belongs on the
// tree nodes that name one.
expect(source("../../sidebar/SidebarTreeRuntimeHost.vue")).toContain('t("docs.title"), action: openDocs');
});
it("keeps the entry off per-object menus", () => {
// openDocs never read the row's table: a table-level entry advertised a
// scope the viewer cannot render.
expect(source("../ObjectBrowser.vue")).not.toContain("docs.title");
});
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `npx vitest run apps/desktop/src/components/objects/__tests__/docsMenuPlacement.spec.ts`
Expected: FAIL, both cases. The first fails because `SidebarTreeRuntimeHost.vue` has no `docs.title`; the second fails because `ObjectBrowser.vue` still has two.
- [ ] **Step 3: Register the entry in the sidebar**
In `apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue`:
Add `BookOpen,` as the first name in the `@lucide/vue` import block, immediately after `import {` on line 13. The block is not alphabetically ordered, so position is a matter of readability only:
```ts
import {
BookOpen,
Database,
ChevronsDown,
```
Add `openDocs` to the destructure on line 312, keeping alphabetical order:
```ts
const { openAllDatabasesExport, openDataCompare, openDatabaseExport, openDatabaseSearch, openDiagram, openDocs, openFieldLineage, openScheduledBackups, openSchemaDiff, openSqlFileExecution, openStructureEditor, openTableImport, openTransfer } = useSidebarTreeToolRuntime({
```
Replace lines 4204-4206:
```ts
if (canOpenDiagram.value) {
items.push({ label: t("diagram.open"), action: openDiagram, icon: Network });
items.push({ label: t("docs.title"), action: openDocs, icon: BookOpen });
}
```
- [ ] **Step 4: Remove the object browser entries**
In `apps/desktop/src/components/objects/ObjectBrowser.vue`:
Delete `BookOpen,` from the icon import on line 9. It has no other use in the file.
Delete the `openDocs` helper, lines 1466-1474 inclusive, together with the blank line that followed it:
```ts
function openDocs(row: ObjectBrowserRow) {
// The docs viewer documents the whole schema rather than one object, so the
// row only supplies which schema to collect.
connectionStore.docsSource = {
connectionId: props.connection.id,
database: props.database,
schema: row.schema || selectedSchema.value,
};
}
```
In `getTableMenuItems`, collapse lines 2666-2671 back to the single-entry form:
```ts
...(canOpenDiagram.value ? [{ label: t("diagram.open"), action: () => openDiagram(item), icon: Network }] : []),
```
In `getViewMenuItems`, collapse lines 2721-2726 to exactly the same single line.
- [ ] **Step 5: Run the placement test to verify it passes**
Run: `npx vitest run apps/desktop/src/components/objects/__tests__/docsMenuPlacement.spec.ts`
Expected: PASS, 2 tests.
- [ ] **Step 6: Typecheck**
Run: `pnpm typecheck`
Expected: no errors. This is what catches a stale `BookOpen` reference or a mistyped `openDocs` in the destructure. An unused-import error here means step 4 removed a menu entry but left the icon, or the reverse.
- [ ] **Step 7: Run the surrounding suites**
Run: `npx vitest run apps/desktop/src/composables apps/desktop/src/components/objects apps/desktop/src/components/sidebar`
Expected: PASS. No existing spec asserts on the docs menu entries, so nothing should need updating. If a sidebar spec fails on menu-item counts or ordering, that spec is the source of truth for the menu's shape — read it before changing it.
- [ ] **Step 8: Commit**
```bash
git add apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue apps/desktop/src/components/objects/ObjectBrowser.vue apps/desktop/src/components/objects/__tests__/docsMenuPlacement.spec.ts
git commit -m "fix(docs): move Documentation to the database context menu"
```
---
## Manual verification
Automated cover stops at "the right store field gets the right value". Confirm the dialog end of the wire once, by hand:
```bash
pnpm dev
```
1. Right-click a PostgreSQL database node in the connections sidebar. "Documentation" appears directly below "View Diagram", with a book icon.
2. Click it. The viewer opens listing tables from **every** schema in that database, not just `public`.
3. Close it, right-click a schema node, pick "Documentation". Only that schema's tables are listed.
4. Open the object browser for the same database and right-click a table. There is no "Documentation" entry; "View Diagram" is still there.
5. Right-click a Redis or MongoDB connection's database node. No "Documentation" entry, because `supportsSchemaDiagram` is false for those drivers — the same visibility set that shipped in v0.5.77.
## Pull request
Target `t8y2/dbx`, base `main`. The description should state that PR #5559's entry point never used the row's table (`ObjectBrowser.vue:1466` read only `row.schema`), so this is a placement correction rather than a feature change, and should flag the follow-up the spec records: documentation visibility currently rides on `supportsSchemaDiagram`, and a dedicated `docs` capability key in `database-drivers.manifest.json` is worth considering separately.

View File

@ -0,0 +1,150 @@
# Documentation Menu at Database Scope Design
**Date:** 2026-08-07
## Goal
Move the "Documentation" context-menu entry from the object browser's table and
view menus to the sidebar's database and schema context menu, so the entry
appears at the scope it actually documents.
## Background
PR #5559 ("feat(docs): database documentation viewer and DBML export") shipped in
v0.5.77. It added the only entry point for the docs viewer to
`ObjectBrowser.vue`, in the table menu and the view menu, nested inside the
existing `canOpenDiagram` branch alongside "View Diagram".
That placement misrepresents what the action does. `openDocs(row)` in
`ObjectBrowser.vue:1466` reads only `row.schema`; the row's table is discarded.
`DatabaseDocsDialog.vue` accepts `prefillConnectionId`, `prefillDatabase`, and
`prefillSchema` — there is no table prop, so the viewer cannot deep-link to a
table even in principle. A user who right-clicks `orders` and picks
"Documentation" gets documentation for the whole schema.
The docs collector was designed for the wider scope. `DatabaseDocsDialog.vue:91`
carries the comment "An absent schema means 'everything the collector finds'",
and `connectionStore.docsSource.schema` is already optional. Database-scoped
documentation is the case the backend was built for and is currently
unreachable from the UI.
Users therefore look for the entry on the database node in the connections
sidebar, where "View Diagram" already sits, and do not find it.
## Requirements
- offer "Documentation" on sidebar database nodes, documenting the whole database
- offer "Documentation" on sidebar schema nodes, documenting that schema only
- remove the entry from the object browser's table and view menus
- keep the set of drivers that expose the entry identical to v0.5.77
- change no backend, dialog, or viewer code
- leave the "View Diagram" entries in both menus untouched
## Non-Goals
- deep-linking the viewer to a single table (would need a new dialog prop and a
viewer route change; a separate change if it is ever wanted)
- introducing a `docs` driver capability key (see Gating below)
- any change to `crates/dbx-core`
## Design
### Sidebar action
`useSidebarTreeToolRuntime.ts` gains `openDocs`, mirroring `openDiagram` at
line 55:
```ts
function openDocs() {
const node = activeNode.value;
if (!node.connectionId || !node.database) return;
connectionStore.docsSource = {
connectionId: node.connectionId,
database: node.database,
schema: node.schema,
};
}
```
`node.schema` carries the whole behaviour. It is `undefined` on a database node,
so `DatabaseDocsDialog.vue:93` (`props.prefillSchema ? [props.prefillSchema] : []`)
sends an empty schema list and the collector documents the entire database. On a
schema node it is set, and the collector is limited to that schema.
`openDiagram` also passes `tableName`; `openDocs` does not. The docs dialog has
no corresponding prop, so passing it would be dead data.
### Menu registration
One `items.push` in `SidebarTreeRuntimeHost.vue`, immediately after the
"View Diagram" push at line 4205:
```ts
if (canOpenDiagram.value) {
items.push({ label: t("diagram.open"), action: openDiagram, icon: Network });
items.push({ label: t("docs.title"), action: openDocs, icon: BookOpen });
}
```
Line 4205 sits inside `buildDatabaseSidebarMenu` (lines 4147-4259), whose guard
is `node.type === "database" || node.type === "schema"`. A single push therefore
covers both node types. The other "View Diagram" push, at line 4483, belongs to
`buildObjectSidebarMenu` and is not touched.
`openDocs` is destructured from `useSidebarTreeToolRuntime` at line 312, and
`BookOpen` is added to the existing `@lucide/vue` icon import (lines 13-57).
### Object browser removal
`ObjectBrowser.vue` loses the docs entry from the table menu (line 2669) and the
view menu (line 2724), the `openDocs` function (line 1466), and the now-orphaned
`BookOpen` import (line 9). The `canOpenDiagram` ternaries collapse back to the
single-element form they had before PR #5559.
### Gating
The new entry reuses the sidebar's existing `canOpenDiagram` computed
(line 3386): `!!activeNode.value.database && supportsSchemaDiagram(currentDatabaseType())`.
This is a deliberate compromise rather than a principled gate. Documentation and
diagrams are separate features; a driver that supported one but not the other
would be misrepresented. The principled alternative is a `docs` key in
`DATABASE_PRODUCT_CAPABILITY_KEYS`, but that list is backed by
`crates/dbx-core/assets/database-drivers.manifest.json`, shared with the Rust
side, and would require a per-support-level default plus per-driver overrides
across every driver.
Reusing the diagram gate keeps this change's visible behaviour identical to
v0.5.77 except for *where* the entry appears, which is the point of the change.
A capability key is worth proposing separately; it is called out in the PR
description rather than bundled here.
## Testing
New spec `apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts`,
following the `shallowRef` node plus stub-store pattern of the neighbouring
`useSidebarTreeExportRuntime.spec.ts`:
- database node: `docsSource` receives `connectionId` and `database`, and
`schema` is `undefined`
- schema node: `schema` is propagated
- node without `database`: `docsSource` is left untouched
No i18n work is required. `docs.title` was added to all eight locales by
PR #5559, so this change introduces no locale-drift exposure.
Verification:
```
pnpm typecheck
npx vitest run apps/desktop/src/composables apps/desktop/src/components/objects
```
## Delivery
Branch `fix/docs-menu-database-scope`, cut from `upstream/main` at `29fe57143`.
One commit, then a PR to `t8y2/dbx`.
No `VERSION` or `CHANGELOG.md` bump: this repository has no changelog, and
`package.json` version is managed by upstream's `scripts/release.mjs`. Version
numbers belong to the maintainer's release process, not to a contribution PR.