Compare commits

..

1 Commits

Author SHA1 Message Date
agent-company 40ce557752 refactor: wire Dashboard, ListIssues, and ListPulls to use template files
Replace inline fmt.Sprintf HTML generation in Dashboard, ListIssues,
and ListPulls handlers with template.ParseFiles rendering of
dashboard.html, issues.html, and pulls.html respectively.

ListIssues now reads ?org= and ?state= query params to filter results.
ListPulls now reads ?org= query param to filter results.

Closes leeworks-agents/gitea-mobile#34

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:45:44 +00:00
7 changed files with 186 additions and 476 deletions
+25 -134
View File
@@ -308,47 +308,26 @@ func (c *Client) ListOrgsAndRepos(ctx context.Context, token string) (map[string
return result, nil return result, nil
} }
// PageSize is the number of items returned per page for paginated listings. // ListAllIssues fetches all open issues across all repos in the given orgs,
const PageSize = 20 // using concurrent requests with a semaphore.
func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string) ([]Issue, error) {
// PaginatedIssues holds a page of issues along with pagination metadata. cacheKey := fmt.Sprintf("issues-%s", strings.Join(orgs, ","))
type PaginatedIssues struct {
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 { if cached, ok := c.getFromCache(cacheKey); ok {
allIssues = cached.([]Issue) return cached.([]Issue), nil
} 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 PaginatedIssues{}, fmt.Errorf("listing repos for %s: %w", org, err) return nil, 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
@@ -361,7 +340,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=%s&type=issues&limit=50", r.FullName, state) path := fmt.Sprintf("/repos/%s/issues?state=open&type=issues&limit=50", r.FullName)
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()
@@ -398,7 +377,7 @@ func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string,
wg.Wait() wg.Wait()
if firstErr != nil { if firstErr != nil {
return PaginatedIssues{}, firstErr return nil, firstErr
} }
// Sort by updated time, newest first. // Sort by updated time, newest first.
@@ -407,46 +386,26 @@ func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string,
}) })
c.setCache(cacheKey, allIssues) c.setCache(cacheKey, allIssues)
return allIssues, nil
} }
// Paginate. // ListAllPullRequests fetches all open PRs across all repos in the given orgs.
start := (page - 1) * PageSize func (c *Client) ListAllPullRequests(ctx context.Context, token string, orgs []string) ([]PullRequest, error) {
if start >= len(allIssues) { cacheKey := fmt.Sprintf("pulls-%s", strings.Join(orgs, ","))
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 { if cached, ok := c.getFromCache(cacheKey); ok {
allPRs = cached.([]PullRequest) return cached.([]PullRequest), nil
} 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 PaginatedPulls{}, fmt.Errorf("listing repos for %s: %w", org, err) return nil, 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
@@ -459,7 +418,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=%s&limit=50", r.FullName, state) path := fmt.Sprintf("/repos/%s/pulls?state=open&limit=50", r.FullName)
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()
@@ -495,7 +454,7 @@ func (c *Client) ListAllPullRequests(ctx context.Context, token string, orgs []s
wg.Wait() wg.Wait()
if firstErr != nil { if firstErr != nil {
return PaginatedPulls{}, firstErr return nil, firstErr
} }
sort.Slice(allPRs, func(i, j int) bool { sort.Slice(allPRs, func(i, j int) bool {
@@ -503,49 +462,20 @@ 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) {
// Collect all open issues across all pages. issues, err := c.ListAllIssues(ctx, token, orgs)
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
}
}
// Collect all open PRs across all pages. prs, err := c.ListAllPullRequests(ctx, token, orgs)
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
@@ -821,45 +751,6 @@ func (c *Client) PostComment(ctx context.Context, token, owner, repo string, ind
return &comment, nil return &comment, nil
} }
// RenderMarkdown renders raw markdown text to HTML using the Gitea API.
// Falls back to the raw text if the API call fails.
func (c *Client) RenderMarkdown(ctx context.Context, token, text string) (string, error) {
payload, err := json.Marshal(map[string]string{
"Text": text,
"Mode": "gfm",
})
if err != nil {
return text, fmt.Errorf("marshaling markdown request: %w", err)
}
url := c.baseURL + "/api/v1/markdown"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(payload)))
if err != nil {
return text, fmt.Errorf("creating markdown request: %w", err)
}
req.Header.Set("Authorization", "token "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/html")
resp, err := c.httpClient.Do(req)
if err != nil {
return text, fmt.Errorf("executing markdown request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return text, fmt.Errorf("markdown API error %d", resp.StatusCode)
}
rendered, err := io.ReadAll(resp.Body)
if err != nil {
return text, fmt.Errorf("reading markdown response: %w", err)
}
return string(rendered), nil
}
// priorityScore returns a numeric score for sorting (lower = higher priority). // priorityScore returns a numeric score for sorting (lower = higher priority).
func priorityScore(labels []string) int { func priorityScore(labels []string) int {
for _, l := range labels { for _, l := range labels {
+4 -134
View File
@@ -38,7 +38,6 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
// Issues. // Issues.
mux.HandleFunc("GET /issues", h.ListIssues) mux.HandleFunc("GET /issues", h.ListIssues)
mux.HandleFunc("GET /issues/new", h.NewIssue)
mux.HandleFunc("POST /issues", h.CreateIssue) mux.HandleFunc("POST /issues", h.CreateIssue)
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels) mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/close", h.CloseIssue) mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/close", h.CloseIssue)
@@ -239,10 +238,6 @@ 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,
@@ -259,37 +254,14 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
queryOrgs = []string{selectedOrg} queryOrgs = []string{selectedOrg}
} }
result, err := h.Client.ListAllIssues(r.Context(), token, queryOrgs, selectedState, page) issues, err := h.Client.ListAllIssues(r.Context(), token, queryOrgs)
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 = result.Issues data.Issues = 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 {
@@ -317,16 +289,10 @@ 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,
@@ -341,37 +307,14 @@ 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) prs, err := h.Client.ListAllPullRequests(r.Context(), token, queryOrgs)
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 = result.Pulls data.Pulls = prs
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 {
@@ -423,17 +366,6 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
labels = nil labels = nil
} }
// Render markdown body if present.
var renderedBody template.HTML
if issue.Body != "" {
rendered, err := h.Client.RenderMarkdown(r.Context(), token, issue.Body)
if err != nil {
slog.Warn("failed to render issue body markdown, using plain text", "error", err)
} else {
renderedBody = template.HTML(rendered)
}
}
// Build the content HTML using the template. // Build the content HTML using the template.
tmpl, err := template.ParseFiles("internal/templates/issue_detail.html") tmpl, err := template.ParseFiles("internal/templates/issue_detail.html")
if err != nil { if err != nil {
@@ -444,14 +376,12 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
type templateData struct { type templateData struct {
Issue *giteaclient.Issue Issue *giteaclient.Issue
RenderedBody template.HTML
Comments []giteaclient.Comment Comments []giteaclient.Comment
AvailableLabels []giteaclient.Label AvailableLabels []giteaclient.Label
} }
data := templateData{ data := templateData{
Issue: issue, Issue: issue,
RenderedBody: renderedBody,
Comments: comments, Comments: comments,
AvailableLabels: labels, AvailableLabels: labels,
} }
@@ -487,17 +417,6 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
return return
} }
// Render markdown body if present.
var renderedBody template.HTML
if pr.Body != "" {
rendered, err := h.Client.RenderMarkdown(r.Context(), token, pr.Body)
if err != nil {
slog.Warn("failed to render PR body markdown, using plain text", "error", err)
} else {
renderedBody = template.HTML(rendered)
}
}
// 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 {
@@ -508,12 +427,10 @@ 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
} }
data := templateData{ data := templateData{
Pull: pr, Pull: pr,
RenderedBody: renderedBody,
} }
var buf strings.Builder var buf strings.Builder
@@ -526,41 +443,6 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
renderPage(w, r, fmt.Sprintf("PR #%d", index), "pulls", buf.String()) renderPage(w, r, fmt.Sprintf("PR #%d", index), "pulls", buf.String())
} }
// NewIssue handles GET /issues/new — renders the create-issue form.
func (h *Handler) NewIssue(w http.ResponseWriter, r *http.Request) {
token := getToken(r)
repos, err := h.Client.ListOrgsAndRepos(r.Context(), token)
if err != nil {
slog.Error("failed to list repos for new issue form", "error", err)
renderPage(w, r, "New Issue", "issues",
`<h1>New Issue</h1><p class="empty">Error loading repositories.</p>`)
return
}
tmpl, err := template.ParseFiles("internal/templates/create_issue.html")
if err != nil {
slog.Error("failed to parse create_issue template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
type templateData struct {
Repos map[string][]giteaclient.Repo
}
data := templateData{Repos: repos}
var buf strings.Builder
if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
slog.Error("failed to execute create_issue template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return
}
renderPage(w, r, "New Issue", "issues", buf.String())
}
// CreateIssue handles POST /issues. // CreateIssue handles POST /issues.
func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) { func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
token := getToken(r) token := getToken(r)
@@ -575,12 +457,6 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
body := r.FormValue("body") body := r.FormValue("body")
if owner == "" || repo == "" || title == "" { if owner == "" || repo == "" || title == "" {
if isHTMX(r) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, `<span class="empty">owner, repo, and title are required</span>`)
return
}
http.Error(w, "owner, repo, and title are required", http.StatusBadRequest) http.Error(w, "owner, repo, and title are required", http.StatusBadRequest)
return return
} }
@@ -588,12 +464,6 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
issue, err := h.Client.CreateIssue(r.Context(), token, owner, repo, title, body, nil) issue, err := h.Client.CreateIssue(r.Context(), token, owner, repo, title, body, nil)
if err != nil { if err != nil {
slog.Error("failed to create issue", "error", err) slog.Error("failed to create issue", "error", err)
if isHTMX(r) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, `<span class="empty">Failed to create issue. Please try again.</span>`)
return
}
http.Error(w, "failed to create issue", http.StatusInternalServerError) http.Error(w, "failed to create issue", http.StatusInternalServerError)
return return
} }
+6 -40
View File
@@ -1,9 +1,7 @@
{{define "content"}} {{define "content"}}
<h1>Create Issue</h1> <h1>Create Issue</h1>
<div id="form-error" class="empty" style="display:none;"></div> <form hx-post="/issues" hx-swap="innerHTML" hx-target="#main-content">
<form id="create-issue-form" hx-post="/issues" hx-swap="innerHTML" hx-target="#main-content">
<div class="form-group"> <div class="form-group">
<label for="repo-select">Repository</label> <label for="repo-select">Repository</label>
<select id="repo-select" name="owner_repo" required> <select id="repo-select" name="owner_repo" required>
@@ -34,43 +32,11 @@
</form> </form>
<script> <script>
(function() { // Split owner/repo from select into hidden fields.
var repoSelect = document.getElementById('repo-select'); document.getElementById('repo-select').addEventListener('change', function() {
var ownerInput = document.getElementById('owner-input'); var parts = this.value.split('/');
var repoInput = document.getElementById('repo-input'); document.getElementById('owner-input').value = parts[0] || '';
var formError = document.getElementById('form-error'); document.getElementById('repo-input').value = parts[1] || '';
function splitOwnerRepo() {
var val = repoSelect.value;
if (val) {
var parts = val.split('/');
ownerInput.value = parts[0] || '';
repoInput.value = parts[1] || '';
} else {
ownerInput.value = '';
repoInput.value = '';
}
}
repoSelect.addEventListener('change', splitOwnerRepo);
// Validate before HTMX submit.
document.getElementById('create-issue-form').addEventListener('htmx:configRequest', function(evt) {
splitOwnerRepo();
if (!ownerInput.value || !repoInput.value) {
evt.preventDefault();
formError.textContent = 'Please select a repository before submitting.';
formError.style.display = 'block';
return false;
}
formError.style.display = 'none';
}); });
// Show server-side errors inline on HTMX error responses.
document.getElementById('create-issue-form').addEventListener('htmx:responseError', function(evt) {
formError.textContent = evt.detail.xhr.responseText || 'An error occurred. Please try again.';
formError.style.display = 'block';
});
})();
</script> </script>
{{end}} {{end}}
+1 -3
View File
@@ -9,9 +9,7 @@
<span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span> <span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span>
{{end}} {{end}}
</div> </div>
{{if .RenderedBody}} {{if .Issue.Body}}
<div class="card-body markdown-body">{{.RenderedBody}}</div>
{{else if .Issue.Body}}
<div class="card-body">{{.Issue.Body}}</div> <div class="card-body">{{.Issue.Body}}</div>
{{end}} {{end}}
</div> </div>
+19 -23
View File
@@ -1,25 +1,3 @@
{{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>
@@ -42,7 +20,25 @@
<p class="empty">No issues found.</p> <p class="empty">No issues found.</p>
{{else}} {{else}}
<div id="issue-list"> <div id="issue-list">
{{template "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}}
</div> </div>
{{end}} {{end}}
{{end}} {{end}}
+1 -3
View File
@@ -15,9 +15,7 @@
<span class="diff-del">-{{.Pull.Deletions}}</span> <span class="diff-del">-{{.Pull.Deletions}}</span>
{{if .Pull.Mergeable}}<span style="color:var(--accent-green);">Mergeable</span>{{end}} {{if .Pull.Mergeable}}<span style="color:var(--accent-green);">Mergeable</span>{{end}}
</div> </div>
{{if .RenderedBody}} {{if .Pull.Body}}
<div class="card-body markdown-body">{{.RenderedBody}}</div>
{{else if .Pull.Body}}
<div class="card-body">{{.Pull.Body}}</div> <div class="card-body">{{.Pull.Body}}</div>
{{end}} {{end}}
</div> </div>
+17 -26
View File
@@ -1,28 +1,3 @@
{{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>
@@ -41,7 +16,23 @@
<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">
{{template "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}}
</div> </div>
{{end}} {{end}}
{{end}} {{end}}