fix(docs): move Documentation to database context menu
This commit is contained in:
parent
9a255aa135
commit
bdf87ac255
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
45
apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts
vendored
Normal file
45
apps/desktop/src/composables/__tests__/useSidebarTreeToolRuntime.docs.spec.ts
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
Loading…
Reference in New Issue