appview: add issue search endpoint #496

merged
opened by boltless.me targeting master from boltless.me/core: feat/search
Changed files
+121 -40
appview
db
issues
pages
templates
repo
issues
+84 -37
appview/db/issues.go
···
import (
"database/sql"
"fmt"
+
"strconv"
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.sh/tangled.sh/core/api/tangled"
+
"tangled.sh/tangled.sh/core/appview/models"
"tangled.sh/tangled.sh/core/appview/pagination"
)
···
return issues, nil
}
-
func GetIssuesPaginated(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
-
var issues []Issue
+
// GetIssueIDs gets list of all existing issue's IDs
+
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
+
var ids []int64
+
+
var filters []filter
openValue := 0
-
if isOpen {
+
if opts.IsOpen {
openValue = 1
}
+
filters = append(filters, FilterEq("open", openValue))
+
if opts.RepoAt != "" {
+
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
+
}
-
rows, err := e.Query(
+
var conditions []string
+
var args []any
+
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
query := fmt.Sprintf(
`
-
with numbered_issue as (
-
select
-
i.id,
-
i.owner_did,
-
i.rkey,
-
i.issue_id,
-
i.created,
-
i.title,
-
i.body,
-
i.open,
-
count(c.id) as comment_count,
-
row_number() over (order by i.created desc) as row_num
-
from
-
issues i
-
left join
-
comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
-
where
-
i.repo_at = ? and i.open = ?
-
group by
-
i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open
-
)
select
-
id,
-
owner_did,
-
rkey,
-
issue_id,
-
created,
-
title,
-
body,
-
open,
-
comment_count
+
id
from
-
numbered_issue
+
issues
+
%s
+
limit ? offset ?`,
+
whereClause,
+
)
+
args = append(args, opts.Page.Limit, opts.Page.Offset)
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
for rows.Next() {
+
var id int64
+
err := rows.Scan(&id)
+
if err != nil {
+
return nil, err
+
}
+
+
ids = append(ids, id)
+
}
+
+
return ids, nil
+
}
+
+
// GetIssuesByIDs gets list of issues from given IDs
+
func GetIssuesByIDs(e Execer, ids []int64) ([]Issue, error) {
+
var issues []Issue
+
+
// HACK: would be better to create "?,?,?,..." or use something like sqlx
+
idStrings := make([]string, len(ids))
+
for i, id := range ids {
+
idStrings[i] = strconv.FormatInt(id, 10)
+
}
+
idList := strings.Join(idStrings, ",")
+
query := fmt.Sprintf(
+
`
+
select
+
i.id,
+
i.owner_did,
+
i.rkey,
+
i.issue_id,
+
i.created,
+
i.title,
+
i.body,
+
i.open,
+
count(c.id) as comment_count
+
from
+
issues i
+
left join
+
comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
where
-
row_num between ? and ?`,
-
repoAt, openValue, page.Offset+1, page.Offset+page.Limit)
+
i.id in (%s)
+
group by
+
i.id
+
order by i.created desc`,
+
idList,
+
)
+
rows, err := e.Query(query)
if err != nil {
return nil, err
}
+28 -1
appview/issues/issues.go
···
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
issues_indexer "tangled.sh/tangled.sh/core/appview/indexer/issues"
+
"tangled.sh/tangled.sh/core/appview/models"
"tangled.sh/tangled.sh/core/appview/notify"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
···
return
}
-
issues, err := db.GetIssuesPaginated(rp.db, f.RepoAt(), isOpen, page)
+
keyword := params.Get("q")
+
+
var ids []int64
+
searchOpts := models.IssueSearchOptions{
+
Keyword: keyword,
+
RepoAt: f.RepoAt().String(),
+
IsOpen: isOpen,
+
Page: page,
+
}
+
if keyword != "" {
+
res, err := rp.indexer.Search(r.Context(), searchOpts)
+
if err != nil {
+
log.Println("failed to search for issues", err)
+
return
+
}
+
log.Println("searched issues:", res.Hits)
+
ids = res.Hits
+
} else {
+
ids, err = db.GetIssueIDs(rp.db, searchOpts)
+
if err != nil {
+
log.Println("failed to search for issues", err)
+
return
+
}
+
}
+
+
issues, err := db.GetIssuesByIDs(rp.db, ids)
if err != nil {
log.Println("failed to get issues", err)
rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
···
RepoInfo: f.RepoInfo(user),
Issues: issues,
FilteringByOpen: isOpen,
+
FilterQuery: keyword,
Page: page,
})
}
+9 -2
appview/pages/templates/repo/issues/issues.html
···
{{ i "ban" "w-4 h-4" }}
<span>{{ .RepoInfo.Stats.IssueCount.Closed }} closed</span>
</a>
+
<form class="flex gap-4" method="GET">
+
<input type="hidden" name="state" value="{{ if .FilteringByOpen }}open{{ else }}closed{{ end }}">
+
<input class="" type="text" name="q" value="{{ .FilterQuery }}">
+
<button class="btn" type="submit">
+
search
+
</button>
+
</form>
</div>
<a
href="/{{ .RepoInfo.FullName }}/issues/new"
···
<a
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
hx-boost="true"
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}"
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}"
>
{{ i "chevron-left" "w-4 h-4" }}
previous
···
<a
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
hx-boost="true"
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}"
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}"
>
next
{{ i "chevron-right" "w-4 h-4" }}