Compare commits

..

1 Commits

Author SHA1 Message Date
agent-company d433801da6 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:08:58 +00:00
2 changed files with 116 additions and 165 deletions
+109 -124
View File
@@ -180,158 +180,155 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
token := getToken(r) token := getToken(r)
orgs := h.getUserOrgs(r) orgs := h.getUserOrgs(r)
type dashboardData struct {
Items []giteaclient.TriageItem
Error string
}
var data dashboardData
if len(orgs) == 0 { if len(orgs) == 0 {
renderPage(w, r, "Dashboard", "dashboard", data.Error = "No organizations found. Check your token permissions."
`<h1>Dashboard</h1><p class="empty">No organizations found. Check your token permissions.</p>`) } else {
return queue, err := h.Client.GetTriageQueue(r.Context(), token, orgs)
if err != nil {
slog.Error("failed to get triage queue", "error", err)
data.Error = "Error loading triage queue."
} else {
data.Items = queue
}
} }
queue, err := h.Client.GetTriageQueue(r.Context(), token, orgs) tmpl, err := template.ParseFiles("internal/templates/dashboard.html")
if err != nil { if err != nil {
slog.Error("failed to get triage queue", "error", err) slog.Error("failed to parse dashboard template", "error", err)
renderPage(w, r, "Dashboard", "dashboard", http.Error(w, "template error", http.StatusInternalServerError)
`<h1>Dashboard</h1><p class="empty">Error loading triage queue.</p>`)
return return
} }
if len(queue) == 0 { var buf strings.Builder
renderPage(w, r, "Dashboard", "dashboard", if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
`<h1>Dashboard</h1><p class="empty">No items need attention. Nice work!</p>`) slog.Error("failed to execute dashboard template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return return
} }
content := `<h1>Dashboard</h1>` renderPage(w, r, "Dashboard", "dashboard", buf.String())
for _, item := range queue {
typeBadge := `<span class="type-badge type-issue">issue</span>`
if item.Type == "pull" {
typeBadge = `<span class="type-badge type-pull">PR</span>`
}
labels := ""
for _, l := range item.Labels {
color := "#8b949e"
switch l {
case "P1":
color = "#f85149"
case "P2":
color = "#d29922"
case "P3":
color = "#58a6ff"
}
labels += fmt.Sprintf(`<span class="label" style="color:%s;border:1px solid %s">%s</span>`, color, color, template.HTMLEscapeString(l))
}
content += fmt.Sprintf(`<div class="card">
<div class="card-title">%s %s</div>
<div class="card-meta">%s/%s #%d %s</div>
</div>`, typeBadge, template.HTMLEscapeString(item.Title),
template.HTMLEscapeString(item.RepoOwner),
template.HTMLEscapeString(item.RepoName),
item.Number, labels)
}
renderPage(w, r, "Dashboard", "dashboard", content)
} }
// ListIssues handles GET /issues. // ListIssues handles GET /issues.
func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
token := getToken(r) token := getToken(r)
orgs := h.getUserOrgs(r) orgNames := h.getUserOrgs(r)
if len(orgs) == 0 { type issuesData struct {
renderPage(w, r, "Issues", "issues", Issues []giteaclient.Issue
`<h1>Issues</h1><p class="empty">No organizations found.</p>`) Orgs []string
return SelectedOrg string
SelectedState string
HasMore bool
NextPage int
Error string
} }
issues, err := h.Client.ListAllIssues(r.Context(), token, orgs) selectedOrg := r.URL.Query().Get("org")
selectedState := r.URL.Query().Get("state")
if selectedState == "" {
selectedState = "open"
}
data := issuesData{
Orgs: orgNames,
SelectedOrg: selectedOrg,
SelectedState: selectedState,
}
if len(orgNames) == 0 {
data.Error = "No organizations found."
} else {
// Filter to selected org if specified.
queryOrgs := orgNames
if selectedOrg != "" {
queryOrgs = []string{selectedOrg}
}
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 = issues
}
}
tmpl, err := template.ParseFiles("internal/templates/issues.html")
if err != nil { if err != nil {
slog.Error("failed to list issues", "error", err) slog.Error("failed to parse issues template", "error", err)
renderPage(w, r, "Issues", "issues", http.Error(w, "template error", http.StatusInternalServerError)
`<h1>Issues</h1><p class="empty">Error loading issues.</p>`)
return return
} }
if len(issues) == 0 { var buf strings.Builder
renderPage(w, r, "Issues", "issues", if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
`<h1>Issues</h1><p class="empty">No open issues found.</p>`) slog.Error("failed to execute issues template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return return
} }
content := `<h1>Issues</h1>` renderPage(w, r, "Issues", "issues", buf.String())
for _, issue := range issues {
labels := ""
for _, l := range issue.Labels {
labels += fmt.Sprintf(`<span class="label" style="color:#%s;border:1px solid #%s">%s</span>`,
l.Color, l.Color, template.HTMLEscapeString(l.Name))
}
assignee := ""
if issue.Assignee != nil {
assignee = fmt.Sprintf(` &middot; %s`, template.HTMLEscapeString(issue.Assignee.Login))
}
content += fmt.Sprintf(`<div class="card">
<div class="card-title">%s</div>
<div class="card-meta">%s/%s #%d %s%s</div>
</div>`, template.HTMLEscapeString(issue.Title),
template.HTMLEscapeString(issue.RepoOwner),
template.HTMLEscapeString(issue.RepoName),
issue.Number, labels, assignee)
}
renderPage(w, r, "Issues", "issues", content)
} }
// ListPulls handles GET /pulls. // ListPulls handles GET /pulls.
func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) { func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
token := getToken(r) token := getToken(r)
orgs := h.getUserOrgs(r) orgNames := h.getUserOrgs(r)
if len(orgs) == 0 { type pullsData struct {
renderPage(w, r, "Pull Requests", "pulls", Pulls []giteaclient.PullRequest
`<h1>Pull Requests</h1><p class="empty">No organizations found.</p>`) Orgs []string
return SelectedOrg string
Error string
} }
prs, err := h.Client.ListAllPullRequests(r.Context(), token, orgs) selectedOrg := r.URL.Query().Get("org")
data := pullsData{
Orgs: orgNames,
SelectedOrg: selectedOrg,
}
if len(orgNames) == 0 {
data.Error = "No organizations found."
} else {
queryOrgs := orgNames
if selectedOrg != "" {
queryOrgs = []string{selectedOrg}
}
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 = prs
}
}
tmpl, err := template.ParseFiles("internal/templates/pulls.html")
if err != nil { if err != nil {
slog.Error("failed to list pull requests", "error", err) slog.Error("failed to parse pulls template", "error", err)
renderPage(w, r, "Pull Requests", "pulls", http.Error(w, "template error", http.StatusInternalServerError)
`<h1>Pull Requests</h1><p class="empty">Error loading pull requests.</p>`)
return return
} }
if len(prs) == 0 { var buf strings.Builder
renderPage(w, r, "Pull Requests", "pulls", if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
`<h1>Pull Requests</h1><p class="empty">No open pull requests found.</p>`) slog.Error("failed to execute pulls template", "error", err)
http.Error(w, "template error", http.StatusInternalServerError)
return return
} }
content := `<h1>Pull Requests</h1>` renderPage(w, r, "Pull Requests", "pulls", buf.String())
for _, pr := range prs {
labels := ""
for _, l := range pr.Labels {
labels += fmt.Sprintf(`<span class="label" style="color:#%s;border:1px solid #%s">%s</span>`,
l.Color, l.Color, template.HTMLEscapeString(l.Name))
}
stats := fmt.Sprintf(`<span style="color:#3fb950">+%d</span> <span style="color:#f85149">-%d</span>`, pr.Additions, pr.Deletions)
mergeStatus := ""
if pr.Mergeable {
mergeStatus = `<span style="color:#3fb950;font-size:0.7rem;">mergeable</span>`
}
content += fmt.Sprintf(`<div class="card">
<div class="card-title"><span class="type-badge type-pull">PR</span> %s</div>
<div class="card-meta">%s/%s #%d %s %s %s</div>
</div>`, template.HTMLEscapeString(pr.Title),
template.HTMLEscapeString(pr.RepoOwner),
template.HTMLEscapeString(pr.RepoName),
pr.Number, labels, stats, mergeStatus)
}
renderPage(w, r, "Pull Requests", "pulls", content)
} }
// IssueDetail handles GET /issues/{owner}/{repo}/{index}. // IssueDetail handles GET /issues/{owner}/{repo}/{index}.
@@ -458,12 +455,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
} }
@@ -471,12 +462,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
} }
+7 -41
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}}