Commit b893d66995

b893d66995a2371a7b623657b89e2ddea6bd8789

parent: ee971df409

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-11 15:44 UTC

web: org label and milestone pages under /{org}/-/, org mark on repository pages

Ref #203
internal/httpd/orglabels.go added +92
@@ -0,0 +1,92 @@
1package httpd
2
3import (
4 "html/template"
5 "net/http"
6
7 "gitbay.org/gitbay/internal/control"
8 "gitbay.org/gitbay/internal/store"
9)
10
11// orgScope resolves the org for its labels or milestones page. Members
12// see it; anyone else only when some repository under the org is
13// readable. Everything else is not found, the same answer as for a
14// user owner or an unknown name.
15func (s *Server) orgScope(w http.ResponseWriter, r *http.Request) (store.Org, store.User, []int64, bool) {
16 viewer := s.viewer(r)
17 org, err := s.st.OrgByName(r.PathValue("owner"))
18 if err != nil {
19 s.notFound(w, r)
20 return org, viewer, nil, false
21 }
22 readable, err := control.ReadableOrgRepoIDs(s.st, viewer, org.ID)
23 if err != nil {
24 http.Error(w, "internal error", http.StatusInternalServerError)
25 return org, viewer, nil, false
26 }
27 role := ""
28 if viewer.ID != 0 {
29 role, _ = s.st.OrgRole(org.ID, viewer.ID)
30 }
31 if role == "" && len(readable) == 0 {
32 s.notFound(w, r)
33 return org, viewer, nil, false
34 }
35 return org, viewer, readable, true
36}
37
38func (s *Server) orgLabels(w http.ResponseWriter, r *http.Request) {
39 org, viewer, readable, ok := s.orgScope(w, r)
40 if !ok {
41 return
42 }
43 labels, err := s.st.ListOrgLabels(org.ID, readable)
44 if err != nil {
45 http.Error(w, "internal error", http.StatusInternalServerError)
46 return
47 }
48 stored := make(map[string]string, len(labels))
49 for _, l := range labels {
50 stored[l.Name] = l.Color
51 }
52 s.render(w, "orglabels.html", struct {
53 basePage
54 Org string
55 Labels []store.Label
56 LabelColors map[string]template.CSS
57 }{s.baseFor(viewer), org.Name, labels, colorStyles(stored)})
58}
59
60func (s *Server) orgMilestones(w http.ResponseWriter, r *http.Request) {
61 org, viewer, readable, ok := s.orgScope(w, r)
62 if !ok {
63 return
64 }
65 state := r.URL.Query().Get("state")
66 if state != "closed" && state != "all" {
67 state = "open"
68 }
69 ms, err := s.st.ListOrgMilestones(org.ID, state, readable)
70 if err != nil {
71 http.Error(w, "internal error", http.StatusInternalServerError)
72 return
73 }
74 type msView struct {
75 store.Milestone
76 Percent int
77 }
78 var views []msView
79 for _, m := range ms {
80 v := msView{Milestone: m}
81 if total := m.OpenItems + m.ClosedItems; total > 0 {
82 v.Percent = m.ClosedItems * 100 / total
83 }
84 views = append(views, v)
85 }
86 s.render(w, "orgmilestones.html", struct {
87 basePage
88 Org string
89 State string
90 Milestones []msView
91 }{s.baseFor(viewer), org.Name, state, views})
92}
internal/httpd/routes.go +2
@@ -71,6 +71,8 @@ func (s *Server) Routes() []Route {
7171 Route{Method: "GET", Pattern: "/{owner}/{repo}/log.atom", Handler: s.logAtom},
7272 Route{Method: "GET", Pattern: "/{owner}/{repo}/log.atom/{ref...}", Handler: s.logAtom},
7373 Route{Method: "GET", Pattern: "/{owner}/activity.atom", Handler: s.ownerAtom},
74 Route{Method: "GET", Pattern: "/{owner}/-/labels", Handler: s.orgLabels},
75 Route{Method: "GET", Pattern: "/{owner}/-/milestones", Handler: s.orgMilestones},
7476 Route{Method: "GET", Pattern: "/{owner}/{repo}/builds", Handler: s.builds},
7577 Route{Method: "GET", Pattern: "/{owner}/{repo}/badge/build.svg", Handler: s.buildBadge},
7678 Route{Method: "GET", Pattern: "/{owner}/{repo}/badge/build.png", Handler: s.buildBadgePNG},
internal/web/templates/labels.html +4 −4
@@ -5,18 +5,18 @@
55{{if .Labels}}<div class="tablewrap"><table class="keys">
66<tr class="cols"><th scope="col">label</th><th scope="col">colour</th><th scope="col">issues</th><th scope="col"></th></tr>
77{{range .Labels}}<tr>
8 <td><a class="chip label" style="{{index $.LabelColors .Name}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/issues?label={{.Name}}">{{.Name}}</a></td>
9 <td>{{if $.CanWrite}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline">
8 <td><a class="chip label" style="{{index $.LabelColors .Name}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/issues?label={{.Name}}">{{.Name}}</a>{{if .Org}} <span class="chip chip-neutral">org</span>{{end}}</td>
9 <td>{{if and $.CanWrite (not .Org)}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline">
1010 <input type="hidden" name="name" value="{{.Name}}">
1111 <input type="text" name="color" value="{{.Color}}" aria-label="Colour for {{.Name}}" placeholder="rrggbb" size="8">
1212 <button type="submit" class="btn">Save</button>
1313 </form>{{else}}<span class="mono">{{if .Color}}{{.Color}}{{else}}—{{end}}</span>{{end}}</td>
1414 <td>{{.Issues}}</td>
15 <td class="act">{{if $.CanWrite}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline">
15 <td class="act">{{if and $.CanWrite (not .Org)}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline">
1616 <input type="hidden" name="action" value="remove">
1717 <input type="hidden" name="name" value="{{.Name}}">
1818 <button type="submit" class="linklike">Remove</button>
19 </form>{{end}}</td>
19 </form>{{else if .Org}}<a href="/{{$.Repo.OwnerName}}/-/labels">org</a>{{end}}</td>
2020</tr>
2121{{end}}</table></div>
2222{{else}}<p class="none">No labels yet.</p>{{end}}
internal/web/templates/milestones.html +1 −1
@@ -11,7 +11,7 @@
1111<ul class="milestonelist">
1212{{range .Milestones}}<li>
1313 <div class="msmain">
14 <p class="title">{{.Title}} <span class="chip {{if eq .State "open"}}chip-open{{else}}chip-done{{end}}">{{.State}}</span></p>
14 <p class="title">{{.Title}} <span class="chip {{if eq .State "open"}}chip-open{{else}}chip-done{{end}}">{{.State}}</span>{{if .OrgID}} <span class="chip chip-neutral">org</span>{{end}}</p>
1515 {{if .Description}}<p class="desc">{{.Description}}</p>{{end}}
1616 <p class="meta">{{if .DueDate}}due {{.DueDate}} · {{end}}{{.ClosedItems}} closed, {{.OpenItems}} open · {{.Percent}}%</p>
1717 <div class="progress"><div class="bar" style="width: {{.Percent}}%"></div></div>
internal/web/templates/orglabels.html added +14
@@ -0,0 +1,14 @@
1{{define "title"}}labels · {{.Org}}{{end}}
2{{define "content"}}
3<h1><a href="/{{.Org}}">{{.Org}}</a> labels</h1>
4<p class="meta">Every repository under {{.Org}} sees these beside its own. Managed with <code>gitbay org label set {{.Org}} &lt;label&gt;</code>; counts span the repositories you can read.</p>
5{{if .Labels}}<div class="tablewrap"><table class="keys">
6<tr class="cols"><th scope="col">label</th><th scope="col">colour</th><th scope="col">issues</th></tr>
7{{range .Labels}}<tr>
8 <td><span class="chip label" style="{{index $.LabelColors .Name}}">{{.Name}}</span></td>
9 <td><span class="mono">{{if .Color}}{{.Color}}{{else}}—{{end}}</span></td>
10 <td>{{.Issues}}</td>
11</tr>
12{{end}}</table></div>
13{{else}}<p class="none">No org labels yet.</p>{{end}}
14{{end}}
internal/web/templates/orgmilestones.html added +23
@@ -0,0 +1,23 @@
1{{define "title"}}milestones · {{.Org}}{{end}}
2{{define "content"}}
3<div class="listhead">
4 <h1><a href="/{{.Org}}">{{.Org}}</a> milestones</h1>
5 <nav class="filters">
6 <a {{if eq .State "open"}}class="active" aria-current="page" {{end}}href="?state=open">open</a>
7 <a {{if eq .State "closed"}}class="active" aria-current="page" {{end}}href="?state=closed">closed</a>
8 <a {{if eq .State "all"}}class="active" aria-current="page" {{end}}href="?state=all">all</a>
9 </nav>
10</div>
11<p class="meta">Progress spans the repositories under {{.Org}} you can read.</p>
12<ul class="milestonelist">
13{{range .Milestones}}<li>
14 <div class="msmain">
15 <p class="title">{{.Title}} <span class="chip {{if eq .State "open"}}chip-open{{else}}chip-done{{end}}">{{.State}}</span></p>
16 {{if .Description}}<p class="desc">{{.Description}}</p>{{end}}
17 <p class="meta">{{if .DueDate}}due {{.DueDate}} · {{end}}{{.ClosedItems}} closed, {{.OpenItems}} open · {{.Percent}}%</p>
18 <div class="progress"><div class="bar" style="width: {{.Percent}}%"></div></div>
19 </div>
20</li>
21{{else}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}org milestones — create one with <code>gitbay org milestone create {{.Org}} "v1.0"</code></li>{{end}}
22</ul>
23{{end}}
internal/web/templates/owner.html +1
@@ -7,6 +7,7 @@
77{{if .Profile.Links}}<p class="meta">{{range $i, $l := .Profile.Links}}{{if $i}} · {{end}}<a href="{{$l.URL}}" rel="nofollow me">{{if $l.Label}}{{$l.Label}}{{else}}{{$l.URL}}{{end}}</a>{{end}}</p>{{end}}
88{{if .Orgs}}<p class="meta">member of {{range .Orgs}}<a class="memberchip" href="/{{.Name}}">{{.Name}}</a> {{end}}</p>{{end}}
99{{if .Members}}<p class="meta">members {{range .Members}}<a class="memberchip" href="/{{.Name}}">{{.Name}} <span class="role">{{.Role}}</span></a> {{end}}</p>{{end}}
10{{if eq .Kind "org"}}<p class="meta"><a href="/{{.Owner}}/-/labels">labels</a> · <a href="/{{.Owner}}/-/milestones">milestones</a></p>{{end}}
1011</section>
1112{{if .AboutHTML}}<section class="readme"><div class="rendered">{{.AboutHTML}}</div></section>{{end}}
1213<section class="activity">