Press [ in detail view to open link picker showing all [[links]] in the current entry. Enter follows a link, resolving by title then body substring. Navigation history stack enables esc to pop back through followed links before returning to list. Adds Store.ResolveLink() for non-transactional link resolution from the TUI layer.
This commit is contained in:
@@ -55,6 +55,26 @@ func syncLinks(ctx context.Context, tx *sql.Tx, s *Store, entityID string, body
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ResolveLink(ctx context.Context, linkText string) (*Entity, error) {
|
||||
lower := strings.ToLower(linkText)
|
||||
|
||||
var id string
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT id FROM entities
|
||||
WHERE LOWER(title) = ? AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC LIMIT 1`, lower).Scan(&id)
|
||||
if err != nil {
|
||||
err = s.db.QueryRowContext(ctx, `
|
||||
SELECT id FROM entities
|
||||
WHERE LOWER(body) LIKE ? AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC LIMIT 1`, "%"+lower+"%").Scan(&id)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return s.Get(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) LoadBacklinks(ctx context.Context, entityID string) ([]Backlink, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT e.id, e.title, e.body, el.link_text
|
||||
|
||||
@@ -223,3 +223,50 @@ func TestSyncLinks_DeletedSourceHidden(t *testing.T) {
|
||||
t.Fatalf("soft-deleted source should not appear in backlinks, got %d", len(backlinks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLink_TitleMatch(t *testing.T) {
|
||||
s := testStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
title := "nginx config"
|
||||
target := &Entity{Body: "proxy_pass details", Title: &title, Glyph: "note", Tags: []string{}}
|
||||
if err := s.Create(ctx, target); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resolved, err := s.ResolveLink(ctx, "nginx config")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolved.ID != target.ID {
|
||||
t.Errorf("resolved ID = %s, want %s", resolved.ID, target.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLink_BodyFallback(t *testing.T) {
|
||||
s := testStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
target := &Entity{Body: "deploy staging checklist", Glyph: "note", Tags: []string{}}
|
||||
if err := s.Create(ctx, target); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resolved, err := s.ResolveLink(ctx, "deploy staging")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolved.ID != target.ID {
|
||||
t.Errorf("resolved ID = %s, want %s", resolved.ID, target.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLink_NotFound(t *testing.T) {
|
||||
s := testStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := s.ResolveLink(ctx, "nonexistent entry")
|
||||
if err != ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user