feat: add backend pagination support for infinite scroll in issues and pulls
Update ListAllIssues and ListAllPullRequests to accept state and page parameters, returning paginated results (20 per page) with HasMore metadata. ListIssues and ListPulls handlers now read page, org, and state query params; HTMX requests for page > 1 return only card HTML fragments for seamless infinite scroll. Both templates extract a reusable "cards" block and pulls.html gains a scroll sentinel matching the existing issues.html pattern. Filter changes reset to page 1. Closes leeworks-agents/gitea-mobile#32 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+95
-25
@@ -308,26 +308,47 @@ func (c *Client) ListOrgsAndRepos(ctx context.Context, token string) (map[string
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAllIssues fetches all open issues across all repos in the given orgs,
|
// PageSize is the number of items returned per page for paginated listings.
|
||||||
// using concurrent requests with a semaphore.
|
const PageSize = 20
|
||||||
func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string) ([]Issue, error) {
|
|
||||||
cacheKey := fmt.Sprintf("issues-%s", strings.Join(orgs, ","))
|
// PaginatedIssues holds a page of issues along with pagination metadata.
|
||||||
if cached, ok := c.getFromCache(cacheKey); ok {
|
type PaginatedIssues struct {
|
||||||
return cached.([]Issue), nil
|
Issues []Issue
|
||||||
|
HasMore bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginatedPulls holds a page of pull requests along with pagination metadata.
|
||||||
|
type PaginatedPulls struct {
|
||||||
|
Pulls []PullRequest
|
||||||
|
HasMore bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllIssues fetches issues across all repos in the given orgs,
|
||||||
|
// using concurrent requests with a semaphore. Results are paginated.
|
||||||
|
func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string, state string, page int) (PaginatedIssues, error) {
|
||||||
|
if state == "" {
|
||||||
|
state = "open"
|
||||||
|
}
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("issues-%s-%s", state, strings.Join(orgs, ","))
|
||||||
|
var allIssues []Issue
|
||||||
|
if cached, ok := c.getFromCache(cacheKey); ok {
|
||||||
|
allIssues = cached.([]Issue)
|
||||||
|
} else {
|
||||||
// First, collect all repos for the given orgs.
|
// First, collect all repos for the given orgs.
|
||||||
var allRepos []Repo
|
var allRepos []Repo
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
repos, err := c.ListOrgRepos(ctx, token, org)
|
repos, err := c.ListOrgRepos(ctx, token, org)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("listing repos for %s: %w", org, err)
|
return PaginatedIssues{}, fmt.Errorf("listing repos for %s: %w", org, err)
|
||||||
}
|
}
|
||||||
allRepos = append(allRepos, repos...)
|
allRepos = append(allRepos, repos...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fan out issue fetching across repos.
|
// Fan out issue fetching across repos.
|
||||||
var allIssues []Issue
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
sem := make(chan struct{}, c.maxConcurrent)
|
sem := make(chan struct{}, c.maxConcurrent)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -340,7 +361,7 @@ func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string)
|
|||||||
sem <- struct{}{}
|
sem <- struct{}{}
|
||||||
defer func() { <-sem }()
|
defer func() { <-sem }()
|
||||||
|
|
||||||
path := fmt.Sprintf("/repos/%s/issues?state=open&type=issues&limit=50", r.FullName)
|
path := fmt.Sprintf("/repos/%s/issues?state=%s&type=issues&limit=50", r.FullName, state)
|
||||||
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
|
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -377,7 +398,7 @@ func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string)
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if firstErr != nil {
|
if firstErr != nil {
|
||||||
return nil, firstErr
|
return PaginatedIssues{}, firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by updated time, newest first.
|
// Sort by updated time, newest first.
|
||||||
@@ -386,26 +407,46 @@ func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string)
|
|||||||
})
|
})
|
||||||
|
|
||||||
c.setCache(cacheKey, allIssues)
|
c.setCache(cacheKey, allIssues)
|
||||||
return allIssues, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAllPullRequests fetches all open PRs across all repos in the given orgs.
|
|
||||||
func (c *Client) ListAllPullRequests(ctx context.Context, token string, orgs []string) ([]PullRequest, error) {
|
|
||||||
cacheKey := fmt.Sprintf("pulls-%s", strings.Join(orgs, ","))
|
|
||||||
if cached, ok := c.getFromCache(cacheKey); ok {
|
|
||||||
return cached.([]PullRequest), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paginate.
|
||||||
|
start := (page - 1) * PageSize
|
||||||
|
if start >= len(allIssues) {
|
||||||
|
return PaginatedIssues{}, nil
|
||||||
|
}
|
||||||
|
end := start + PageSize
|
||||||
|
hasMore := end < len(allIssues)
|
||||||
|
if end > len(allIssues) {
|
||||||
|
end = len(allIssues)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaginatedIssues{Issues: allIssues[start:end], HasMore: hasMore}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllPullRequests fetches PRs across all repos in the given orgs.
|
||||||
|
// Results are paginated.
|
||||||
|
func (c *Client) ListAllPullRequests(ctx context.Context, token string, orgs []string, state string, page int) (PaginatedPulls, error) {
|
||||||
|
if state == "" {
|
||||||
|
state = "open"
|
||||||
|
}
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("pulls-%s-%s", state, strings.Join(orgs, ","))
|
||||||
|
var allPRs []PullRequest
|
||||||
|
if cached, ok := c.getFromCache(cacheKey); ok {
|
||||||
|
allPRs = cached.([]PullRequest)
|
||||||
|
} else {
|
||||||
var allRepos []Repo
|
var allRepos []Repo
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
repos, err := c.ListOrgRepos(ctx, token, org)
|
repos, err := c.ListOrgRepos(ctx, token, org)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("listing repos for %s: %w", org, err)
|
return PaginatedPulls{}, fmt.Errorf("listing repos for %s: %w", org, err)
|
||||||
}
|
}
|
||||||
allRepos = append(allRepos, repos...)
|
allRepos = append(allRepos, repos...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var allPRs []PullRequest
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
sem := make(chan struct{}, c.maxConcurrent)
|
sem := make(chan struct{}, c.maxConcurrent)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -418,7 +459,7 @@ func (c *Client) ListAllPullRequests(ctx context.Context, token string, orgs []s
|
|||||||
sem <- struct{}{}
|
sem <- struct{}{}
|
||||||
defer func() { <-sem }()
|
defer func() { <-sem }()
|
||||||
|
|
||||||
path := fmt.Sprintf("/repos/%s/pulls?state=open&limit=50", r.FullName)
|
path := fmt.Sprintf("/repos/%s/pulls?state=%s&limit=50", r.FullName, state)
|
||||||
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
|
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -454,7 +495,7 @@ func (c *Client) ListAllPullRequests(ctx context.Context, token string, orgs []s
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if firstErr != nil {
|
if firstErr != nil {
|
||||||
return nil, firstErr
|
return PaginatedPulls{}, firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(allPRs, func(i, j int) bool {
|
sort.Slice(allPRs, func(i, j int) bool {
|
||||||
@@ -462,20 +503,49 @@ func (c *Client) ListAllPullRequests(ctx context.Context, token string, orgs []s
|
|||||||
})
|
})
|
||||||
|
|
||||||
c.setCache(cacheKey, allPRs)
|
c.setCache(cacheKey, allPRs)
|
||||||
return allPRs, nil
|
}
|
||||||
|
|
||||||
|
// Paginate.
|
||||||
|
start := (page - 1) * PageSize
|
||||||
|
if start >= len(allPRs) {
|
||||||
|
return PaginatedPulls{}, nil
|
||||||
|
}
|
||||||
|
end := start + PageSize
|
||||||
|
hasMore := end < len(allPRs)
|
||||||
|
if end > len(allPRs) {
|
||||||
|
end = len(allPRs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaginatedPulls{Pulls: allPRs[start:end], HasMore: hasMore}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTriageQueue returns unassigned issues and PRs needing review, sorted by priority.
|
// GetTriageQueue returns unassigned issues and PRs needing review, sorted by priority.
|
||||||
func (c *Client) GetTriageQueue(ctx context.Context, token string, orgs []string) ([]TriageItem, error) {
|
func (c *Client) GetTriageQueue(ctx context.Context, token string, orgs []string) ([]TriageItem, error) {
|
||||||
issues, err := c.ListAllIssues(ctx, token, orgs)
|
// Collect all open issues across all pages.
|
||||||
|
var issues []Issue
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
result, err := c.ListAllIssues(ctx, token, orgs, "open", page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching issues for triage: %w", err)
|
return nil, fmt.Errorf("fetching issues for triage: %w", err)
|
||||||
}
|
}
|
||||||
|
issues = append(issues, result.Issues...)
|
||||||
|
if !result.HasMore {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prs, err := c.ListAllPullRequests(ctx, token, orgs)
|
// Collect all open PRs across all pages.
|
||||||
|
var prs []PullRequest
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
result, err := c.ListAllPullRequests(ctx, token, orgs, "open", page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching PRs for triage: %w", err)
|
return nil, fmt.Errorf("fetching PRs for triage: %w", err)
|
||||||
}
|
}
|
||||||
|
prs = append(prs, result.Pulls...)
|
||||||
|
if !result.HasMore {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var queue []TriageItem
|
var queue []TriageItem
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,10 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
|
|||||||
if selectedState == "" {
|
if selectedState == "" {
|
||||||
selectedState = "open"
|
selectedState = "open"
|
||||||
}
|
}
|
||||||
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
data := issuesData{
|
data := issuesData{
|
||||||
Orgs: orgNames,
|
Orgs: orgNames,
|
||||||
@@ -255,14 +259,37 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
|
|||||||
queryOrgs = []string{selectedOrg}
|
queryOrgs = []string{selectedOrg}
|
||||||
}
|
}
|
||||||
|
|
||||||
issues, err := h.Client.ListAllIssues(r.Context(), token, queryOrgs)
|
result, err := h.Client.ListAllIssues(r.Context(), token, queryOrgs, selectedState, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to list issues", "error", err)
|
slog.Error("failed to list issues", "error", err)
|
||||||
data.Error = "Error loading issues."
|
data.Error = "Error loading issues."
|
||||||
} else {
|
} else {
|
||||||
data.Issues = issues
|
data.Issues = result.Issues
|
||||||
|
data.HasMore = result.HasMore
|
||||||
|
if result.HasMore {
|
||||||
|
data.NextPage = page + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HTMX infinite-scroll requests (page > 1), return only the card fragment.
|
||||||
|
if isHTMX(r) && page > 1 {
|
||||||
|
tmpl, err := template.ParseFiles("internal/templates/issues.html")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to parse issues template", "error", err)
|
||||||
|
http.Error(w, "template error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := tmpl.ExecuteTemplate(&buf, "cards", data); err != nil {
|
||||||
|
slog.Error("failed to execute issues cards template", "error", err)
|
||||||
|
http.Error(w, "template error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
fmt.Fprint(w, buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles("internal/templates/issues.html")
|
tmpl, err := template.ParseFiles("internal/templates/issues.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -290,10 +317,16 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
|
|||||||
Pulls []giteaclient.PullRequest
|
Pulls []giteaclient.PullRequest
|
||||||
Orgs []string
|
Orgs []string
|
||||||
SelectedOrg string
|
SelectedOrg string
|
||||||
|
HasMore bool
|
||||||
|
NextPage int
|
||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedOrg := r.URL.Query().Get("org")
|
selectedOrg := r.URL.Query().Get("org")
|
||||||
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
data := pullsData{
|
data := pullsData{
|
||||||
Orgs: orgNames,
|
Orgs: orgNames,
|
||||||
@@ -308,14 +341,37 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
|
|||||||
queryOrgs = []string{selectedOrg}
|
queryOrgs = []string{selectedOrg}
|
||||||
}
|
}
|
||||||
|
|
||||||
prs, err := h.Client.ListAllPullRequests(r.Context(), token, queryOrgs)
|
result, err := h.Client.ListAllPullRequests(r.Context(), token, queryOrgs, "open", 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."
|
||||||
} else {
|
} else {
|
||||||
data.Pulls = prs
|
data.Pulls = result.Pulls
|
||||||
|
data.HasMore = result.HasMore
|
||||||
|
if result.HasMore {
|
||||||
|
data.NextPage = page + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HTMX infinite-scroll requests (page > 1), return only the card fragment.
|
||||||
|
if isHTMX(r) && page > 1 {
|
||||||
|
tmpl, err := template.ParseFiles("internal/templates/pulls.html")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to parse pulls template", "error", err)
|
||||||
|
http.Error(w, "template error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := tmpl.ExecuteTemplate(&buf, "cards", data); err != nil {
|
||||||
|
slog.Error("failed to execute pulls cards template", "error", err)
|
||||||
|
http.Error(w, "template error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
fmt.Fprint(w, buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles("internal/templates/pulls.html")
|
tmpl, err := template.ParseFiles("internal/templates/pulls.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
{{define "cards"}}
|
||||||
|
{{range .Issues}}
|
||||||
|
<div class="card" hx-get="/issues/{{.RepoOwner}}/{{.RepoName}}/{{.Number}}" hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true">
|
||||||
|
<div class="card-title">{{.Title}}</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span>{{.RepoOwner}}/{{.RepoName}} #{{.Number}}</span>
|
||||||
|
{{range .Labels}}
|
||||||
|
<span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span>
|
||||||
|
{{end}}
|
||||||
|
{{if .Assignee}}
|
||||||
|
<span>{{.Assignee.Login}}</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .HasMore}}
|
||||||
|
<div class="scroll-sentinel" hx-get="/issues?page={{.NextPage}}&org={{.SelectedOrg}}&state={{.SelectedState}}" hx-trigger="revealed" hx-swap="outerHTML" hx-target="this">
|
||||||
|
<div class="spinner htmx-indicator"></div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Issues</h1>
|
<h1>Issues</h1>
|
||||||
|
|
||||||
@@ -20,25 +42,7 @@
|
|||||||
<p class="empty">No issues found.</p>
|
<p class="empty">No issues found.</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div id="issue-list">
|
<div id="issue-list">
|
||||||
{{range .Issues}}
|
{{template "cards" .}}
|
||||||
<div class="card" hx-get="/issues/{{.RepoOwner}}/{{.RepoName}}/{{.Number}}" hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true">
|
|
||||||
<div class="card-title">{{.Title}}</div>
|
|
||||||
<div class="card-meta">
|
|
||||||
<span>{{.RepoOwner}}/{{.RepoName}} #{{.Number}}</span>
|
|
||||||
{{range .Labels}}
|
|
||||||
<span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span>
|
|
||||||
{{end}}
|
|
||||||
{{if .Assignee}}
|
|
||||||
<span>{{.Assignee.Login}}</span>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if .HasMore}}
|
|
||||||
<div class="scroll-sentinel" hx-get="/issues?page={{.NextPage}}&org={{.SelectedOrg}}&state={{.SelectedState}}" hx-trigger="revealed" hx-swap="outerHTML" hx-target="this">
|
|
||||||
<div class="spinner htmx-indicator"></div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
|
{{define "cards"}}
|
||||||
|
{{range .Pulls}}
|
||||||
|
<div class="card" hx-get="/pulls/{{.RepoOwner}}/{{.RepoName}}/{{.Number}}" hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true">
|
||||||
|
<div class="card-title">
|
||||||
|
<span class="type-badge type-pull">PR</span>
|
||||||
|
{{.Title}}
|
||||||
|
</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span>{{.RepoOwner}}/{{.RepoName}} #{{.Number}}</span>
|
||||||
|
{{range .Labels}}
|
||||||
|
<span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span>
|
||||||
|
{{end}}
|
||||||
|
<span class="diff-add">+{{.Additions}}</span>
|
||||||
|
<span class="diff-del">-{{.Deletions}}</span>
|
||||||
|
{{if .Mergeable}}<span style="color:var(--accent-green);font-size:0.7rem;">mergeable</span>{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .HasMore}}
|
||||||
|
<div class="scroll-sentinel" hx-get="/pulls?page={{.NextPage}}&org={{.SelectedOrg}}" hx-trigger="revealed" hx-swap="outerHTML" hx-target="this">
|
||||||
|
<div class="spinner htmx-indicator"></div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>Pull Requests</h1>
|
<h1>Pull Requests</h1>
|
||||||
|
|
||||||
@@ -16,23 +41,7 @@
|
|||||||
<p class="empty">No open pull requests found.</p>
|
<p class="empty">No open pull requests found.</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div id="pull-list">
|
<div id="pull-list">
|
||||||
{{range .Pulls}}
|
{{template "cards" .}}
|
||||||
<div class="card" hx-get="/pulls/{{.RepoOwner}}/{{.RepoName}}/{{.Number}}" hx-target="#main-content" hx-swap="innerHTML" hx-push-url="true">
|
|
||||||
<div class="card-title">
|
|
||||||
<span class="type-badge type-pull">PR</span>
|
|
||||||
{{.Title}}
|
|
||||||
</div>
|
|
||||||
<div class="card-meta">
|
|
||||||
<span>{{.RepoOwner}}/{{.RepoName}} #{{.Number}}</span>
|
|
||||||
{{range .Labels}}
|
|
||||||
<span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span>
|
|
||||||
{{end}}
|
|
||||||
<span class="diff-add">+{{.Additions}}</span>
|
|
||||||
<span class="diff-del">-{{.Deletions}}</span>
|
|
||||||
{{if .Mergeable}}<span style="color:var(--accent-green);font-size:0.7rem;">mergeable</span>{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user