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
+135 -244
View File
@@ -308,243 +308,173 @@ func (c *Client) ListOrgsAndRepos(ctx context.Context, token string) (map[string
return result, nil
}
// PageSize is the number of items returned per page for paginated listings.
const PageSize = 20
// PaginatedIssues holds a page of issues along with pagination metadata.
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
// ListAllIssues fetches all open issues across all repos in the given orgs,
// using concurrent requests with a semaphore.
func (c *Client) ListAllIssues(ctx context.Context, token string, orgs []string) ([]Issue, error) {
cacheKey := fmt.Sprintf("issues-%s", strings.Join(orgs, ","))
if cached, ok := c.getFromCache(cacheKey); ok {
return cached.([]Issue), nil
}
cacheKey := fmt.Sprintf("issues-%s-%s", state, strings.Join(orgs, ","))
// First, collect all repos for the given orgs.
var allRepos []Repo
for _, org := range orgs {
repos, err := c.ListOrgRepos(ctx, token, org)
if err != nil {
return nil, fmt.Errorf("listing repos for %s: %w", org, err)
}
allRepos = append(allRepos, repos...)
}
// Fan out issue fetching across repos.
var allIssues []Issue
if cached, ok := c.getFromCache(cacheKey); ok {
allIssues = cached.([]Issue)
} else {
// First, collect all repos for the given orgs.
var allRepos []Repo
for _, org := range orgs {
repos, err := c.ListOrgRepos(ctx, token, org)
var mu sync.Mutex
sem := make(chan struct{}, c.maxConcurrent)
var wg sync.WaitGroup
var firstErr error
for _, repo := range allRepos {
wg.Add(1)
go func(r Repo) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
path := fmt.Sprintf("/repos/%s/issues?state=open&type=issues&limit=50", r.FullName)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
return PaginatedIssues{}, fmt.Errorf("listing repos for %s: %w", org, err)
}
allRepos = append(allRepos, repos...)
}
// Fan out issue fetching across repos.
var mu sync.Mutex
sem := make(chan struct{}, c.maxConcurrent)
var wg sync.WaitGroup
var firstErr error
for _, repo := range allRepos {
wg.Add(1)
go func(r Repo) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
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)
if err != nil {
mu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("fetching issues for %s: %w", r.FullName, err)
}
mu.Unlock()
return
}
defer resp.Body.Close()
var issues []Issue
if err := json.NewDecoder(resp.Body).Decode(&issues); err != nil {
mu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("decoding issues for %s: %w", r.FullName, err)
}
mu.Unlock()
return
}
// Tag each issue with repo info.
for i := range issues {
issues[i].RepoOwner = r.Owner.Login
issues[i].RepoName = r.Name
}
mu.Lock()
allIssues = append(allIssues, issues...)
if firstErr == nil {
firstErr = fmt.Errorf("fetching issues for %s: %w", r.FullName, err)
}
mu.Unlock()
}(repo)
}
return
}
defer resp.Body.Close()
wg.Wait()
var issues []Issue
if err := json.NewDecoder(resp.Body).Decode(&issues); err != nil {
mu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("decoding issues for %s: %w", r.FullName, err)
}
mu.Unlock()
return
}
if firstErr != nil {
return PaginatedIssues{}, firstErr
}
// Tag each issue with repo info.
for i := range issues {
issues[i].RepoOwner = r.Owner.Login
issues[i].RepoName = r.Name
}
// Sort by updated time, newest first.
sort.Slice(allIssues, func(i, j int) bool {
return allIssues[i].UpdatedAt.After(allIssues[j].UpdatedAt)
})
c.setCache(cacheKey, allIssues)
mu.Lock()
allIssues = append(allIssues, issues...)
mu.Unlock()
}(repo)
}
// 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)
wg.Wait()
if firstErr != nil {
return nil, firstErr
}
return PaginatedIssues{Issues: allIssues[start:end], HasMore: hasMore}, nil
// Sort by updated time, newest first.
sort.Slice(allIssues, func(i, j int) bool {
return allIssues[i].UpdatedAt.After(allIssues[j].UpdatedAt)
})
c.setCache(cacheKey, allIssues)
return allIssues, 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
// 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 {
allPRs = cached.([]PullRequest)
} else {
var allRepos []Repo
for _, org := range orgs {
repos, err := c.ListOrgRepos(ctx, token, org)
return cached.([]PullRequest), nil
}
var allRepos []Repo
for _, org := range orgs {
repos, err := c.ListOrgRepos(ctx, token, org)
if err != nil {
return nil, fmt.Errorf("listing repos for %s: %w", org, err)
}
allRepos = append(allRepos, repos...)
}
var allPRs []PullRequest
var mu sync.Mutex
sem := make(chan struct{}, c.maxConcurrent)
var wg sync.WaitGroup
var firstErr error
for _, repo := range allRepos {
wg.Add(1)
go func(r Repo) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
path := fmt.Sprintf("/repos/%s/pulls?state=open&limit=50", r.FullName)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
return PaginatedPulls{}, fmt.Errorf("listing repos for %s: %w", org, err)
}
allRepos = append(allRepos, repos...)
}
var mu sync.Mutex
sem := make(chan struct{}, c.maxConcurrent)
var wg sync.WaitGroup
var firstErr error
for _, repo := range allRepos {
wg.Add(1)
go func(r Repo) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
path := fmt.Sprintf("/repos/%s/pulls?state=%s&limit=50", r.FullName, state)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
mu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("fetching PRs for %s: %w", r.FullName, err)
}
mu.Unlock()
return
}
defer resp.Body.Close()
var prs []PullRequest
if err := json.NewDecoder(resp.Body).Decode(&prs); err != nil {
mu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("decoding PRs for %s: %w", r.FullName, err)
}
mu.Unlock()
return
}
for i := range prs {
prs[i].RepoOwner = r.Owner.Login
prs[i].RepoName = r.Name
}
mu.Lock()
allPRs = append(allPRs, prs...)
if firstErr == nil {
firstErr = fmt.Errorf("fetching PRs for %s: %w", r.FullName, err)
}
mu.Unlock()
}(repo)
}
return
}
defer resp.Body.Close()
wg.Wait()
var prs []PullRequest
if err := json.NewDecoder(resp.Body).Decode(&prs); err != nil {
mu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("decoding PRs for %s: %w", r.FullName, err)
}
mu.Unlock()
return
}
if firstErr != nil {
return PaginatedPulls{}, firstErr
}
for i := range prs {
prs[i].RepoOwner = r.Owner.Login
prs[i].RepoName = r.Name
}
sort.Slice(allPRs, func(i, j int) bool {
return allPRs[i].UpdatedAt.After(allPRs[j].UpdatedAt)
})
c.setCache(cacheKey, allPRs)
mu.Lock()
allPRs = append(allPRs, prs...)
mu.Unlock()
}(repo)
}
// 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)
wg.Wait()
if firstErr != nil {
return nil, firstErr
}
return PaginatedPulls{Pulls: allPRs[start:end], HasMore: hasMore}, nil
sort.Slice(allPRs, func(i, j int) bool {
return allPRs[i].UpdatedAt.After(allPRs[j].UpdatedAt)
})
c.setCache(cacheKey, allPRs)
return allPRs, nil
}
// GetTriageQueue returns unassigned issues and PRs needing review, sorted by priority.
func (c *Client) GetTriageQueue(ctx context.Context, token string, orgs []string) ([]TriageItem, error) {
// 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 {
return nil, fmt.Errorf("fetching issues for triage: %w", err)
}
issues = append(issues, result.Issues...)
if !result.HasMore {
break
}
issues, err := c.ListAllIssues(ctx, token, orgs)
if err != nil {
return nil, fmt.Errorf("fetching issues for triage: %w", err)
}
// 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 {
return nil, fmt.Errorf("fetching PRs for triage: %w", err)
}
prs = append(prs, result.Pulls...)
if !result.HasMore {
break
}
prs, err := c.ListAllPullRequests(ctx, token, orgs)
if err != nil {
return nil, fmt.Errorf("fetching PRs for triage: %w", err)
}
var queue []TriageItem
@@ -821,45 +751,6 @@ func (c *Client) PostComment(ctx context.Context, token, owner, repo string, ind
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).
func priorityScore(labels []string) int {
for _, l := range labels {
+6 -136
View File
@@ -38,7 +38,6 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
// Issues.
mux.HandleFunc("GET /issues", h.ListIssues)
mux.HandleFunc("GET /issues/new", h.NewIssue)
mux.HandleFunc("POST /issues", h.CreateIssue)
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
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 == "" {
selectedState = "open"
}
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
data := issuesData{
Orgs: orgNames,
@@ -259,38 +254,15 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
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 {
slog.Error("failed to list issues", "error", err)
data.Error = "Error loading issues."
} else {
data.Issues = result.Issues
data.HasMore = result.HasMore
if result.HasMore {
data.NextPage = page + 1
}
data.Issues = issues
}
}
// 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")
if err != nil {
slog.Error("failed to parse issues template", "error", err)
@@ -317,16 +289,10 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
Pulls []giteaclient.PullRequest
Orgs []string
SelectedOrg string
HasMore bool
NextPage int
Error string
}
selectedOrg := r.URL.Query().Get("org")
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
data := pullsData{
Orgs: orgNames,
@@ -341,38 +307,15 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
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 {
slog.Error("failed to list pull requests", "error", err)
data.Error = "Error loading pull requests."
} else {
data.Pulls = result.Pulls
data.HasMore = result.HasMore
if result.HasMore {
data.NextPage = page + 1
}
data.Pulls = prs
}
}
// 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")
if err != nil {
slog.Error("failed to parse pulls template", "error", err)
@@ -423,17 +366,6 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
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.
tmpl, err := template.ParseFiles("internal/templates/issue_detail.html")
if err != nil {
@@ -444,14 +376,12 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) {
type templateData struct {
Issue *giteaclient.Issue
RenderedBody template.HTML
Comments []giteaclient.Comment
AvailableLabels []giteaclient.Label
}
data := templateData{
Issue: issue,
RenderedBody: renderedBody,
Comments: comments,
AvailableLabels: labels,
}
@@ -487,17 +417,6 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
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.
tmpl, err := template.ParseFiles("internal/templates/pull_detail.html")
if err != nil {
@@ -507,13 +426,11 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) {
}
type templateData struct {
Pull *giteaclient.PullRequest
RenderedBody template.HTML
Pull *giteaclient.PullRequest
}
data := templateData{
Pull: pr,
RenderedBody: renderedBody,
Pull: pr,
}
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())
}
// 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.
func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
token := getToken(r)
@@ -575,12 +457,6 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
body := r.FormValue("body")
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)
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)
if err != nil {
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)
return
}
+7 -41
View File
@@ -1,9 +1,7 @@
{{define "content"}}
<h1>Create Issue</h1>
<div id="form-error" class="empty" style="display:none;"></div>
<form id="create-issue-form" hx-post="/issues" hx-swap="innerHTML" hx-target="#main-content">
<form hx-post="/issues" hx-swap="innerHTML" hx-target="#main-content">
<div class="form-group">
<label for="repo-select">Repository</label>
<select id="repo-select" name="owner_repo" required>
@@ -34,43 +32,11 @@
</form>
<script>
(function() {
var repoSelect = document.getElementById('repo-select');
var ownerInput = document.getElementById('owner-input');
var repoInput = document.getElementById('repo-input');
var formError = document.getElementById('form-error');
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';
});
})();
// Split owner/repo from select into hidden fields.
document.getElementById('repo-select').addEventListener('change', function() {
var parts = this.value.split('/');
document.getElementById('owner-input').value = parts[0] || '';
document.getElementById('repo-input').value = parts[1] || '';
});
</script>
{{end}}
+1 -3
View File
@@ -9,9 +9,7 @@
<span class="label" style="color:#{{.Color}};border:1px solid #{{.Color}}">{{.Name}}</span>
{{end}}
</div>
{{if .RenderedBody}}
<div class="card-body markdown-body">{{.RenderedBody}}</div>
{{else if .Issue.Body}}
{{if .Issue.Body}}
<div class="card-body">{{.Issue.Body}}</div>
{{end}}
</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"}}
<h1>Issues</h1>
@@ -42,7 +20,25 @@
<p class="empty">No issues found.</p>
{{else}}
<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>
{{end}}
{{end}}
+1 -3
View File
@@ -15,9 +15,7 @@
<span class="diff-del">-{{.Pull.Deletions}}</span>
{{if .Pull.Mergeable}}<span style="color:var(--accent-green);">Mergeable</span>{{end}}
</div>
{{if .RenderedBody}}
<div class="card-body markdown-body">{{.RenderedBody}}</div>
{{else if .Pull.Body}}
{{if .Pull.Body}}
<div class="card-body">{{.Pull.Body}}</div>
{{end}}
</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"}}
<h1>Pull Requests</h1>
@@ -41,7 +16,23 @@
<p class="empty">No open pull requests found.</p>
{{else}}
<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>
{{end}}
{{end}}