Compare commits

...

3 Commits

Author SHA1 Message Date
agent-company 63d0afb4e2 feat: add comments thread to PR detail view
Fetch and display PR comments in the pull request detail page,
using the same Gitea issue comments API endpoint. Shows author,
timestamp, and body for each comment, with a friendly empty state
when no comments exist.

Closes leeworks-agents/gitea-mobile#81

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:04:56 +00:00
AI-Manager 011addea5b Merge pull request 'feat: add open/closed state filter to PR list view' (#75) from feature/pr-state-filter into master
Build and Push / test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
2026-03-27 08:43:04 +00:00
agent-company 7fa7d3f868 feat: add open/closed state filter to PR list view
Mirror the existing issues state filter pattern: read state query param
(default "open"), pass it to ListAllPullRequests instead of hardcoded
"open", and add a state select widget to the pulls filter bar with
proper hx-include for HTMX partial reloads and infinite scroll.

Closes leeworks-agents/gitea-mobile#72

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 08:06:24 +00:00
3 changed files with 48 additions and 12 deletions
+24 -9
View File
@@ -334,23 +334,29 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
orgNames := h.getUserOrgs(r) orgNames := h.getUserOrgs(r)
type pullsData struct { type pullsData struct {
Pulls []giteaclient.PullRequest Pulls []giteaclient.PullRequest
Orgs []string Orgs []string
SelectedOrg string SelectedOrg string
HasMore bool SelectedState string
NextPage int HasMore bool
Error string NextPage int
Error string
} }
selectedOrg := r.URL.Query().Get("org") selectedOrg := r.URL.Query().Get("org")
selectedState := r.URL.Query().Get("state")
if selectedState == "" {
selectedState = "open"
}
page, _ := strconv.Atoi(r.URL.Query().Get("page")) page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 { if page < 1 {
page = 1 page = 1
} }
data := pullsData{ data := pullsData{
Orgs: orgNames, Orgs: orgNames,
SelectedOrg: selectedOrg, SelectedOrg: selectedOrg,
SelectedState: selectedState,
} }
if len(orgNames) == 0 { if len(orgNames) == 0 {
@@ -361,7 +367,7 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
queryOrgs = []string{selectedOrg} queryOrgs = []string{selectedOrg}
} }
result, err := h.Client.ListAllPullRequests(r.Context(), token, queryOrgs, "open", page) result, err := h.Client.ListAllPullRequests(r.Context(), token, queryOrgs, selectedState, page)
if err != nil { if err != nil {
slog.Error("failed to list pull requests", "error", err) slog.Error("failed to list pull requests", "error", err)
data.Error = "Error loading pull requests." data.Error = "Error loading pull requests."
@@ -526,6 +532,13 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
} }
} }
// Fetch comments for this PR (Gitea uses the issues endpoint for PR comments).
comments, err := h.Client.GetIssueComments(r.Context(), token, owner, repo, index)
if err != nil {
slog.Warn("failed to fetch PR comments", "error", err, "owner", owner, "repo", repo, "index", index)
// Non-fatal: continue rendering without comments.
}
// Build the content HTML using the template. // Build the content HTML using the template.
tmpl, err := template.ParseFiles("internal/templates/pull_detail.html") tmpl, err := template.ParseFiles("internal/templates/pull_detail.html")
if err != nil { if err != nil {
@@ -537,11 +550,13 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
type templateData struct { type templateData struct {
Pull *giteaclient.PullRequest Pull *giteaclient.PullRequest
RenderedBody template.HTML RenderedBody template.HTML
Comments []giteaclient.Comment
} }
data := templateData{ data := templateData{
Pull: pr, Pull: pr,
RenderedBody: renderedBody, RenderedBody: renderedBody,
Comments: comments,
} }
var buf strings.Builder var buf strings.Builder
+17
View File
@@ -46,4 +46,21 @@
<button type="submit" class="btn btn-primary">Submit Review</button> <button type="submit" class="btn btn-primary">Submit Review</button>
</form> </form>
</div> </div>
{{if .Comments}}
<h2>Comments</h2>
<div id="comments-list">
{{range .Comments}}
<div class="comment">
<div class="comment-header">
<strong>{{.User}}</strong>
<span>{{.CreatedAt}}</span>
</div>
<div class="comment-body">{{.Body}}</div>
</div>
{{end}}
</div>
{{else}}
<p class="empty" style="margin-top:1rem;">No comments yet.</p>
{{end}}
{{end}} {{end}}
+7 -3
View File
@@ -17,7 +17,7 @@
</div> </div>
{{end}} {{end}}
{{if .HasMore}} {{if .HasMore}}
<div class="scroll-sentinel" hx-get="/pulls?page={{.NextPage}}&org={{.SelectedOrg}}" hx-trigger="revealed" hx-swap="outerHTML" hx-target="this"> <div class="scroll-sentinel" hx-get="/pulls?page={{.NextPage}}&org={{.SelectedOrg}}&state={{.SelectedState}}" hx-trigger="revealed" hx-swap="outerHTML" hx-target="this">
<div class="spinner htmx-indicator"></div> <div class="spinner htmx-indicator"></div>
</div> </div>
{{end}} {{end}}
@@ -27,18 +27,22 @@
<h1>Pull Requests</h1> <h1>Pull Requests</h1>
<div class="filter-bar"> <div class="filter-bar">
<select name="org" hx-get="/pulls" hx-trigger="change" hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true"> <select name="org" hx-get="/pulls" hx-trigger="change" hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true" hx-include="[name='state']">
<option value="">All orgs</option> <option value="">All orgs</option>
{{range .Orgs}} {{range .Orgs}}
<option value="{{.}}" {{if eq . $.SelectedOrg}}selected{{end}}>{{.}}</option> <option value="{{.}}" {{if eq . $.SelectedOrg}}selected{{end}}>{{.}}</option>
{{end}} {{end}}
</select> </select>
<select name="state" hx-get="/pulls" hx-trigger="change" hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true" hx-include="[name='org']">
<option value="open" {{if eq .SelectedState "open"}}selected{{end}}>Open</option>
<option value="closed" {{if eq .SelectedState "closed"}}selected{{end}}>Closed</option>
</select>
</div> </div>
{{if .Error}} {{if .Error}}
<p class="empty">{{.Error}}</p> <p class="empty">{{.Error}}</p>
{{else if not .Pulls}} {{else if not .Pulls}}
<p class="empty">No open pull requests found.</p> <p class="empty">No {{.SelectedState}} pull requests found.</p>
{{else}} {{else}}
<div id="pull-list"> <div id="pull-list">
{{template "cards" .}} {{template "cards" .}}