feat(db): add wiki-link extraction, resolution, and backlinks
CI / test (pull_request) Successful in 2m27s
CI / test (pull_request) Successful in 2m27s
[[wiki-links]] in entry bodies are extracted at save time, resolved to entity IDs (title match first, body substring fallback), and stored in entity_links junction table. Backlinks surface in TUI detail view showing entries that link to the current entry. Schema migration v5 adds entity_links with CASCADE/SET NULL semantics. Links sync on Create, Update, and Absorb.
This commit is contained in:
@@ -55,6 +55,10 @@ type stepsPersistedMsg struct{}
|
||||
|
||||
type templateCopiedMsg struct{}
|
||||
|
||||
type backlinksLoadedMsg struct {
|
||||
backlinks []db.Backlink
|
||||
}
|
||||
|
||||
type tagsLoadedMsg struct {
|
||||
tags []db.TagCount
|
||||
}
|
||||
@@ -194,6 +198,16 @@ func loadTags(store *db.Store) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func loadBacklinks(store *db.Store, entityID string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
backlinks, err := store.LoadBacklinks(context.Background(), entityID)
|
||||
if err != nil {
|
||||
return errMsg{err}
|
||||
}
|
||||
return backlinksLoadedMsg{backlinks}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRailTags(store *db.Store) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
tags, err := store.ListTags(context.Background(), false)
|
||||
|
||||
+28
-7
@@ -21,13 +21,14 @@ const (
|
||||
)
|
||||
|
||||
type detailModel struct {
|
||||
entity *db.Entity
|
||||
scroll int
|
||||
height int
|
||||
width int
|
||||
mode detailMode
|
||||
run runModel
|
||||
fill fillModel
|
||||
entity *db.Entity
|
||||
backlinks []db.Backlink
|
||||
scroll int
|
||||
height int
|
||||
width int
|
||||
mode detailMode
|
||||
run runModel
|
||||
fill fillModel
|
||||
}
|
||||
|
||||
func newDetailModel() detailModel {
|
||||
@@ -36,6 +37,7 @@ func newDetailModel() detailModel {
|
||||
|
||||
func (d *detailModel) setEntity(e *db.Entity) {
|
||||
d.entity = e
|
||||
d.backlinks = nil
|
||||
d.scroll = 0
|
||||
d.mode = detailPreview
|
||||
}
|
||||
@@ -144,6 +146,25 @@ func (d detailModel) previewView(width int) string {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(d.backlinks) > 0 {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(detailLabelStyle.Render(" ← backlinks"))
|
||||
b.WriteString("\n")
|
||||
for _, bl := range d.backlinks {
|
||||
label := bl.Body
|
||||
if bl.Title != nil {
|
||||
label = *bl.Title
|
||||
} else if len(label) > 40 {
|
||||
label = label[:40] + "…"
|
||||
}
|
||||
line := " " + backlinkStyle.Render(label)
|
||||
if bl.LinkText != "" {
|
||||
line += " " + hintDescStyle.Render("(as \""+bl.LinkText+"\")")
|
||||
}
|
||||
b.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
meta := fmt.Sprintf("created %s", e.CreatedAt.Format(time.DateTime))
|
||||
if e.ModifiedAt != e.CreatedAt {
|
||||
|
||||
@@ -287,6 +287,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.detail.mode = detailPreview
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("copied resolved"))
|
||||
|
||||
case backlinksLoadedMsg:
|
||||
if m.detail.entity != nil {
|
||||
m.detail.backlinks = msg.backlinks
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case tagsLoadedMsg:
|
||||
m.filter.setTags(msg.tags)
|
||||
m.tagRail.setTags(msg.tags)
|
||||
@@ -865,6 +871,7 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
} else {
|
||||
m.state = stateDetail
|
||||
}
|
||||
return m, loadBacklinks(m.store, e.ID)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
@@ -1212,6 +1219,7 @@ func (m model) selectedEntity() *db.Entity {
|
||||
func (m model) reloadDetail(id string) tea.Cmd {
|
||||
return tea.Batch(
|
||||
loadEntities(m.store, m.listParams()),
|
||||
loadBacklinks(m.store, id),
|
||||
func() tea.Msg {
|
||||
e, err := m.store.Get(context.Background(), id)
|
||||
if err != nil {
|
||||
|
||||
@@ -43,6 +43,7 @@ var (
|
||||
stumbleAgeStyle lipgloss.Style
|
||||
acSelectedStyle lipgloss.Style
|
||||
acItemStyle lipgloss.Style
|
||||
backlinkStyle lipgloss.Style
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -100,4 +101,5 @@ func applyTheme() {
|
||||
stumbleAgeStyle = lipgloss.NewStyle().Foreground(remind)
|
||||
acSelectedStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
|
||||
acItemStyle = lipgloss.NewStyle().Foreground(muted)
|
||||
backlinkStyle = lipgloss.NewStyle().Foreground(muted)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user