Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions internal/ui/coverage_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,9 @@ func TestERDHelpersCoverage(t *testing.T) {
_ = renderERDList(g, 80, true)
_ = renderERDList(types.ERDGraph{}, 40, true)
_ = renderERDList(g, 80, false)
_ = renderERDDiagram(g, 120)
_ = renderERDDiagram(g, 20)
_ = renderERDDiagram(types.ERDGraph{Tables: []types.ERDTable{{Name: "solo", Columns: []string{"id"}}}}, 80)
_ = renderERDDiagram(g, 120, "")
_ = renderERDDiagram(g, 20, "")
_ = renderERDDiagram(types.ERDGraph{Tables: []types.ERDTable{{Name: "solo", Columns: []string{"id"}}}}, 80, "")
// Isolates + multi-child edges for wrap/bus/partition paths
wide := types.ERDGraph{
Tables: []types.ERDTable{
Expand All @@ -291,7 +291,7 @@ func TestERDHelpersCoverage(t *testing.T) {
{FromTable: "c5", FromCols: []string{"hub_id"}, ToTable: "hub", ToCols: []string{"id"}},
},
}
_ = renderERDDiagram(wide, 100)
_ = renderERDDiagram(wide, 100, "")
linked, isolates := erdPartition(wide)
if len(isolates) < 2 || len(linked.Tables) < 2 {
t.Fatalf("partition linked=%d isolates=%d", len(linked.Tables), len(isolates))
Expand All @@ -307,12 +307,12 @@ func TestERDHelpersCoverage(t *testing.T) {
// Only isolates (no FKs) → diagram shows unconnected section
_ = renderERDDiagram(types.ERDGraph{
Tables: []types.ERDTable{{Name: "x", Columns: []string{"id"}}, {Name: "y", Columns: []string{"n"}}},
}, 80)
}, 80, "")
// Linked tables but empty edges after self-FK filter
_ = renderERDDiagram(types.ERDGraph{
Tables: []types.ERDTable{{Name: "t", Columns: []string{"id", "t_id", "a", "b", "c", "d", "e"}}},
Edges: []types.FKEdge{{FromTable: "t", FromCols: []string{"t_id"}, ToTable: "t", ToCols: []string{"id"}}},
}, 80)
}, 80, "")
// Reverse-layer edge skipped (child.layer <= parent.layer)
_ = layoutERDCanvas(types.ERDGraph{
Tables: []types.ERDTable{{Name: "p", Columns: []string{"id"}}, {Name: "c", Columns: []string{"id", "p_id"}}},
Expand All @@ -321,7 +321,7 @@ func TestERDHelpersCoverage(t *testing.T) {
{FromTable: "p", FromCols: []string{"id"}, ToTable: "c", ToCols: []string{"id"}}, // reverse
{FromTable: "missing_from", FromCols: []string{"x"}, ToTable: "p", ToCols: []string{"id"}},
},
}, [][]string{{"p"}, {"c"}}, map[string]bool{"c.p_id": true, "p.id": true}, 80)
}, [][]string{{"p"}, {"c"}}, map[string]bool{"c.p_id": true, "p.id": true}, 80, "")
_ = erdWrapLayers([][]string{{"a", "b", "c", "d", "e", "f"}}, 4)
_ = erdWrapLayers([][]string{{"a", "b"}}, 4)
_ = erdWrapLayers(nil, 0)
Expand Down Expand Up @@ -365,8 +365,8 @@ func TestERDHelpersCoverage(t *testing.T) {
_ = mergeWireBox('┌', 'x')
n1 := erdNode{name: "a", x: 2, y: 2, w: 12, h: 5, table: types.ERDTable{Name: "a", Columns: []string{"id", "email"}}}
n2 := erdNode{name: "b", x: 20, y: 2, w: 12, h: 5, table: types.ERDTable{Name: "b", Columns: []string{"id", "a_id"}}}
c.drawBox(n1, map[string]bool{"a.id": true})
c.drawBox(n2, map[string]bool{"b.a_id": true})
c.drawBox(n1, map[string]bool{"a.id": true}, false)
c.drawBox(n2, map[string]bool{"b.a_id": true}, false)
c.routeEdge(n1, n2, "fk", 0)
n3 := erdNode{name: "c", x: 2, y: 12, w: 12, h: 4, table: types.ERDTable{Name: "c", Columns: []string{"id"}}}
c.routeEdge(n2, n3, "", 1)
Expand All @@ -392,22 +392,80 @@ func TestERDHelpersCoverage(t *testing.T) {
c2 := newERDCanvas(0, 0)
_ = c2
c3 := newERDCanvas(5, 5)
c3.drawBox(erdNode{name: "t", x: 0, y: 0, w: 10, h: 10, table: types.ERDTable{Name: "t", Columns: []string{"a", "b", "c", "d", "e"}}}, nil)
c3.drawBox(erdNode{name: "t", x: 0, y: 0, w: 10, h: 10, table: types.ERDTable{Name: "t", Columns: []string{"a", "b", "c", "d", "e"}}}, nil, false)
// many columns → ellipsis; FK accent; id plain
c3.drawBox(erdNode{
name: "wide", x: 0, y: 0, w: 14, h: 12,
table: types.ERDTable{Name: "wide", Columns: []string{"id", "a_id", "b_id", "c_id", "d_id", "note"}},
}, map[string]bool{"wide.a_id": true, "wide.b_id": true})
}, map[string]bool{"wide.a_id": true, "wide.b_id": true}, false)
_ = c3.lines()
// empty graph layout + missing table name → nCol==0 path
_ = layoutERDCanvas(types.ERDGraph{}, nil, nil, 40)
_ = layoutERDCanvas(types.ERDGraph{}, [][]string{{"ghost"}}, nil, 40)
_ = layoutERDCanvas(types.ERDGraph{}, nil, nil, 40, "")
_ = layoutERDCanvas(types.ERDGraph{}, [][]string{{"ghost"}}, nil, 40, "")
// nCol > erdMaxCols in layout (many FK cols)
_ = layoutERDCanvas(types.ERDGraph{
Tables: []types.ERDTable{{Name: "fat", Columns: []string{"id", "a_id", "b_id", "c_id", "d_id", "e_id"}}},
}, [][]string{{"fat"}}, map[string]bool{
"fat.a_id": true, "fat.b_id": true, "fat.c_id": true, "fat.d_id": true, "fat.e_id": true,
}, 40)
}, 40, "fat")
// Highlighted focus box
c4 := newERDCanvas(30, 12)
c4.drawBox(erdNode{name: "hub", x: 2, y: 2, w: 12, h: 6, table: types.ERDTable{Name: "hub", Columns: []string{"id"}}}, nil, true)
_ = c4.lines()
// Focus subgraph helpers
fg := types.ERDGraph{
Schema: "public",
Tables: []types.ERDTable{
{Name: "users", Columns: []string{"id"}},
{Name: "orders", Columns: []string{"id", "user_id"}},
{Name: "items", Columns: []string{"id"}},
},
Edges: []types.FKEdge{
{FromTable: "orders", FromCols: []string{"user_id"}, ToTable: "users", ToCols: []string{"id"}},
},
}
sub := erdFocusSubgraph(fg, "users")
if len(sub.Tables) != 2 || len(sub.Edges) != 1 {
t.Fatalf("focus subgraph: %+v", sub)
}
_ = erdFocusSubgraph(fg, "")
_ = erdFocusSubgraph(fg, "missing")
_ = renderERDDiagram(sub, 80, "users")

m := NewModel()
m.ERD = fg
m.Objects = []types.SchemaObject{
{Name: "users", Kind: types.ObjectTable},
{Name: "orders", Kind: types.ObjectTable},
}
m.SelectedObjIdx = 0
m.ERDFocusAll = false
g, focus := m.erdViewGraph()
if focus != "users" || len(g.Tables) != 2 {
t.Fatalf("view graph focus=%q tables=%d", focus, len(g.Tables))
}
m.ERDFocusAll = true
g, focus = m.erdViewGraph()
if focus != "" || len(g.Tables) != 3 {
t.Fatalf("all mode focus=%q tables=%d", focus, len(g.Tables))
}
m.ERDFocusAll = false
m.Objects = []types.SchemaObject{{Name: "nope", Kind: types.ObjectView}}
if m.erdFocusCandidate() != "" {
t.Fatal("view should not focus")
}
m.Objects = nil
if m.erdFocusCandidate() != "" {
t.Fatal("empty selection")
}
m.ERD = fg
m.Objects = []types.SchemaObject{{Name: "users", Kind: types.ObjectTable}}
m.SelectedObjIdx = 0
m.ERDFocusAll = false
m.Width, m.Height = 120, 40
_ = m.viewERDContent(100, 30)
m.ERDFocusAll = true
_ = m.viewERDContent(100, 30)
}

func TestModelHelpersRemainingBranches(t *testing.T) {
Expand Down
22 changes: 22 additions & 0 deletions internal/ui/coverage_keys_extra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,28 @@ func TestExtra_KeysERD(t *testing.T) {
if cmd == nil {
t.Fatal("r openERD")
}
// Focus / all toggles
m.Objects = []types.SchemaObject{{Name: "users", Kind: types.ObjectTable}}
m.SelectedObjIdx = 0
m.ERD = types.ERDGraph{Tables: []types.ERDTable{{Name: "users"}}}
m.Focus = focusContent
m.Screen = types.ScreenERD
nm2, _ := m.keysERD("a")
m = nm2.(Model)
if !m.ERDFocusAll {
t.Fatal("a should show all")
}
nm2, _ = m.keysERD("f")
m = nm2.(Model)
if m.ERDFocusAll {
t.Fatal("f should focus selection")
}
m.Objects = nil
nm2, _ = m.keysERD("f")
m = nm2.(Model)
if m.StatusMsg == "" {
t.Fatal("expected focus hint status")
}
_, _ = m.keysERD("z")
}

Expand Down
2 changes: 1 addition & 1 deletion internal/ui/coverage_render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ func TestCoverageRenderActivityERDServer(t *testing.T) {
},
Edges: []types.FKEdge{{FromTable: "b", FromCols: []string{"a_id"}, ToTable: "a", ToCols: []string{"id"}}},
}
_ = renderERDDiagram(g, 100)
_ = renderERDDiagram(g, 100, "")
_ = erdFindTable(g, "missing")
_ = erdFindTable(g, "a")
_ = orderERDColumns("a", []string{"x", "id", "z"}, map[string]bool{"a.z": true}, false)
Expand Down
16 changes: 8 additions & 8 deletions internal/ui/coverage_view_edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,8 @@ func TestCoverageViewEdgeCases(t *testing.T) {
// same x center → straight down
childStraight := erdNode{x: 4, y: 8, w: 8, h: 3, name: "c", table: types.ERDTable{Name: "c", Columns: []string{"id"}}}
c2 := newERDCanvas(20, 14)
c2.drawBox(parent, nil)
c2.drawBox(childStraight, nil)
c2.drawBox(parent, nil, false)
c2.drawBox(childStraight, nil, false)
c2.routeEdge(parent, childStraight, "fk", 0)
// early exit: child too close
c2.routeEdge(parent, erdNode{x: 4, y: 3, w: 4, h: 2}, "early", 0)
Expand Down Expand Up @@ -553,10 +553,10 @@ func TestCoverageViewEdgeCases(t *testing.T) {
"z", 0)

// renderERDDiagram: empty, isolated, empty cols, missing edge ends, reverse layer edge
_ = renderERDDiagram(types.ERDGraph{}, 80)
_ = renderERDDiagram(types.ERDGraph{}, 80, "")
_ = renderERDDiagram(types.ERDGraph{
Tables: []types.ERDTable{{Name: "solo", Columns: nil}, {Name: "wide", Columns: []string{"id", "a", "b", "c", "d", "e", "f"}}},
}, 80)
}, 80, "")
// max tables → list mode via viewERDContent
m.Screen = types.ScreenERD
m.Loading = true
Expand Down Expand Up @@ -602,8 +602,8 @@ func TestCoverageViewEdgeCases(t *testing.T) {
{FromTable: "ghost", FromCols: []string{"id"}, ToTable: "users", ToCols: []string{"id"}},
},
}
_ = renderERDDiagram(g, 100)
_ = renderERDDiagram(g, 40)
_ = renderERDDiagram(g, 100, "")
_ = renderERDDiagram(g, 40, "")
_ = renderERDList(g, 5, true)
_ = renderERDList(types.ERDGraph{Tables: g.Tables}, 40, true) // no edges

Expand Down Expand Up @@ -673,8 +673,8 @@ func TestCoverageViewEdgeCases(t *testing.T) {
{FromTable: "b1", FromCols: []string{"a_id"}, ToTable: "a1", ToCols: []string{"id"}},
},
}
_ = renderERDDiagram(wideG, 30)
_ = renderERDDiagram(wideG, 120)
_ = renderERDDiagram(wideG, 30, "")
_ = renderERDDiagram(wideG, 120, "")

m.ERD = g
m.CurrentSchema = ""
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/coverage_views_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,8 @@ func TestViewBranchesRemaining(t *testing.T) {
c.put(2, 2, '│', 1)
parent := erdNode{x: 1, y: 1, w: 8, h: 4, name: "p", table: types.ERDTable{Name: "p", Columns: []string{"id"}}}
child := erdNode{x: 1, y: 7, w: 8, h: 3, name: "c", table: types.ERDTable{Name: "c", Columns: []string{"id"}}}
c.drawBox(parent, nil)
c.drawBox(child, map[string]bool{"c.id": true})
c.drawBox(parent, nil, false)
c.drawBox(child, map[string]bool{"c.id": true}, false)
c.routeEdge(parent, child, "fk", 0)
c.routeEdge(parent, erdNode{x: 1, y: 2, w: 4, h: 2}, "early", 0)
_ = c.lines()
Expand Down
6 changes: 3 additions & 3 deletions internal/ui/final_gaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func TestFinalGaps_PaintAndEditor(t *testing.T) {

func TestFinalGaps_ERDComplex(t *testing.T) {
// empty tables
_ = renderERDDiagram(types.ERDGraph{}, 80)
_ = renderERDDiagram(types.ERDGraph{}, 80, "")
// tables with empty columns
g := types.ERDGraph{
Tables: []types.ERDTable{
Expand All @@ -225,8 +225,8 @@ func TestFinalGaps_ERDComplex(t *testing.T) {
{FromTable: "c", FromCols: []string{"id"}, ToTable: "c", ToCols: []string{"id"}},
},
}
_ = renderERDDiagram(g, 100)
_ = renderERDDiagram(g, 30)
_ = renderERDDiagram(g, 100, "")
_ = renderERDDiagram(g, 30, "")

// cycle for stack detection in erdLayers
g2 := types.ERDGraph{
Expand Down
5 changes: 3 additions & 2 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ type Model struct {
Activity []types.ActivityRow
SelectedActIdx int

ERD types.ERDGraph
ERDOffset int
ERD types.ERDGraph
ERDOffset int
ERDFocusAll bool // when true, show full schema; otherwise focus selected table

Favorites []types.Favorite
SelectedFavIdx int
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/residual_100_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,13 @@ func TestConnectionsFramePadAndERDResidual(t *testing.T) {
}
_ = erdBarycenterOrder(g2, erdLayers(g2))
// render diagram maxX expand
_ = renderERDDiagram(g2, 20)
_ = renderERDDiagram(g2, 20, "")
// edge missing node
g3 := types.ERDGraph{
Tables: []types.ERDTable{{Name: "only", Columns: []string{"id"}}},
Edges: []types.FKEdge{{FromTable: "only", ToTable: "nope", FromCols: []string{"id"}, ToCols: []string{"id"}}},
}
_ = renderERDDiagram(g3, 40)
_ = renderERDDiagram(g3, 40, "")
}

func TestSidebarAndHeaderResidual(t *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletions internal/ui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,8 @@ func (m Model) openERD() (tea.Model, tea.Cmd) {
m = m.clearObjectContent()
m.ContentMode = contentPreview
m.ERDOffset = 0
// Prefer neighborhood of the selected sidebar table when available.
m.ERDFocusAll = false
schema := m.CurrentSchema
if schema == "" {
schema = "public"
Expand Down Expand Up @@ -1145,6 +1147,18 @@ func (m Model) keysERD(key string) (tea.Model, tea.Cmd) {
m.ERDOffset = 0
case "r":
return m.openERD()
case "a":
m.ERDFocusAll = true
m.ERDOffset = 0
m.StatusMsg = "ERD: all tables"
case "f":
if m.erdFocusCandidate() == "" {
m.StatusMsg = "Select a table to focus"
return m, nil
}
m.ERDFocusAll = false
m.ERDOffset = 0
m.StatusMsg = "ERD: focus " + m.erdFocusCandidate()
}
return m, nil
}
Expand Down
Loading