web: one register for empty states and contribution hints !501

merged merged by cmc on 2026-09-28 23:00 UTC · krz/gitbay:web-empty-states into main

15 files changed, +269 −17

Layout: unified · split

CHANGELOG.org +6
@@ -6,6 +6,12 @@ anything beyond "replace the binary and restart" is needed.
6 6
7* Unreleased 7* Unreleased
8 8
9- Empty states on the web state the fact instead of a CLI command, and
10 drop "yet" on a finished item; the merge request list offers a New
11 merge request link, a fork link, or a sign-in prompt depending on
12 what the visitor can do; and the search page's scope caption is
13 always visible, not only before a first search (#270).
14
9Credentials and sessions: revocation, delegation, expiry and an idle 15Credentials and sessions: revocation, delegation, expiry and an idle
10timeout (#256, #257, #276, #277). 16timeout (#256, #257, #276, #277).
11 17
e2e/searchweb_test.go +1 −1
@@ -91,7 +91,7 @@ func TestGlobalSearchAndNotificationsWeb(t *testing.T) {
91 t.Fatalf("mark all read: %d", status) 91 t.Fatalf("mark all read: %d", status)
92 } 92 }
93 _, body = browserGet(t, browser, inst.base()+"/notifications") 93 _, body = browserGet(t, browser, inst.base()+"/notifications")
94 if !strings.Contains(body, "nothing unread") { 94 if !strings.Contains(body, "no unread notifications") {
95 t.Fatalf("unread list after sweep:\n%s", body) 95 t.Fatalf("unread list after sweep:\n%s", body)
96 } 96 }
97 _, body = browserGet(t, browser, inst.base()+"/notifications?all=1") 97 _, body = browserGet(t, browser, inst.base()+"/notifications?all=1")
internal/httpd/buildpages_test.go +63
@@ -57,6 +57,69 @@ func TestBuildsPageRendersCommandOutput(t *testing.T) {
57 } 57 }
58} 58}
59 59
60// A writer sees the instruction to add CI; a reader without push access
61// sees only the fact, since the instruction is not theirs to act on
62// (#270).
63func TestBuildsEmptyStateOmitsInstructionForReaders(t *testing.T) {
64 var sb strings.Builder
65 filter := buildFilter{}
66 err := web.Render(&sb, "builds.html", struct {
67 repoPage
68 Builds []control.BuildOut
69 Jobs []control.JobOut
70 Runs []buildRun
71 Filter buildFilter
72 Facets []facetGroup
73 Refs []string
74 Older string
75 CanWrite bool
76 Notice string
77 }{
78 testRepoPage(), nil, nil, nil, filter, nil, nil, "", false, "",
79 })
80 if err != nil {
81 t.Fatalf("render: %v", err)
82 }
83 out := sb.String()
84 if !strings.Contains(out, "no builds") {
85 t.Error(`missing "no builds"`)
86 }
87 if strings.Contains(out, "ci.yml") {
88 t.Error("a reader without push access should not see the push instruction")
89 }
90}
91
92// A writer with zero builds gets the one instruction the page can give
93// them (#270).
94func TestBuildsEmptyStateShowsInstructionForWriters(t *testing.T) {
95 var sb strings.Builder
96 filter := buildFilter{}
97 err := web.Render(&sb, "builds.html", struct {
98 repoPage
99 Builds []control.BuildOut
100 Jobs []control.JobOut
101 Runs []buildRun
102 Filter buildFilter
103 Facets []facetGroup
104 Refs []string
105 Older string
106 CanWrite bool
107 Notice string
108 }{
109 testRepoPage(), nil, nil, nil, filter, nil, nil, "", true, "",
110 })
111 if err != nil {
112 t.Fatalf("render: %v", err)
113 }
114 out := sb.String()
115 if !strings.Contains(out, "no builds") {
116 t.Error(`missing "no builds"`)
117 }
118 if !strings.Contains(out, "push a commit with a .gitbay/ci.yml") {
119 t.Error("a writer with no builds should see the push instruction")
120 }
121}
122
60func TestBuildPageRendersCommandOutput(t *testing.T) { 123func TestBuildPageRendersCommandOutput(t *testing.T) {
61 var sb strings.Builder 124 var sb strings.Builder
62 err := web.Render(&sb, "build.html", buildView{ 125 err := web.Render(&sb, "build.html", buildView{
internal/httpd/mrpage_test.go +60
@@ -83,6 +83,66 @@ func testMR(state string) store.MR {
83 HeadSHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b"} 83 HeadSHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b"}
84} 84}
85 85
86// A finished merge request states an empty label list as a fact, not a
87// promise something is still coming (#270).
88func TestMRPageLabelsNoYetOnFinishedState(t *testing.T) {
89 var sb strings.Builder
90 if err := web.Render(&sb, "mr.html", mrPageData{
91 repoPage: testRepoPage(), MR: testMR("merged"), View: "conversation",
92 }); err != nil {
93 t.Fatalf("render: %v", err)
94 }
95 if !strings.Contains(sb.String(), "no labels") {
96 t.Error(`merged MR with no labels should read "no labels", not "none yet"`)
97 }
98}
99
100// An open MR can still gain labels, so its empty state keeps "none yet"
101// rather than the finished-item wording (#270).
102func TestMRPageLabelsNoneYetOnOpenState(t *testing.T) {
103 var sb strings.Builder
104 if err := web.Render(&sb, "mr.html", mrPageData{
105 repoPage: testRepoPage(), MR: testMR("open"), View: "conversation",
106 }); err != nil {
107 t.Fatalf("render: %v", err)
108 }
109 out := sb.String()
110 if !strings.Contains(out, "none yet") {
111 t.Error(`open MR with no labels should read "none yet"`)
112 }
113 if strings.Contains(out, "no labels") {
114 t.Error(`open MR with no labels should not read "no labels"`)
115 }
116}
117
118// A closed MR is finished the same way a merged one is (#270).
119func TestMRPageLabelsNoLabelsOnClosedState(t *testing.T) {
120 var sb strings.Builder
121 if err := web.Render(&sb, "mr.html", mrPageData{
122 repoPage: testRepoPage(), MR: testMR("closed"), View: "conversation",
123 }); err != nil {
124 t.Fatalf("render: %v", err)
125 }
126 if !strings.Contains(sb.String(), "no labels") {
127 t.Error(`closed MR with no labels should read "no labels", not "none yet"`)
128 }
129}
130
131func TestMRPageReviewersEmptyStateDropsNobody(t *testing.T) {
132 var sb strings.Builder
133 if err := web.Render(&sb, "mr.html", mrPageData{
134 repoPage: testRepoPage(), MR: testMR("open"), View: "conversation",
135 }); err != nil {
136 t.Fatalf("render: %v", err)
137 }
138 if strings.Contains(sb.String(), "nobody yet") {
139 t.Error(`reviewers empty state should read "no reviewers"`)
140 }
141 if !strings.Contains(sb.String(), "no reviewers") {
142 t.Error(`missing "no reviewers"`)
143 }
144}
145
86// The header states what happened to the MR: who opened, merged, or closed 146// The header states what happened to the MR: who opened, merged, or closed
87// it, and when. A merge or close with no stamp (imports, pre-0029 merges) 147// it, and when. A merge or close with no stamp (imports, pre-0029 merges)
88// names the state without claiming a byline it cannot back up. 148// names the state without claiming a byline it cannot back up.
internal/httpd/mrslist_test.go added +86
@@ -0,0 +1,86 @@
1package httpd
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8 "time"
9
10 "gitbay.org/gitbay/internal/config"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// A repository's MR list offers the right next step by access level: a
15// writer gets "New merge request", a signed-in reader without push gets
16// a fork link, and a signed-out visitor gets a sign-in prompt (#270).
17func TestMRsListContributionHintByAccess(t *testing.T) {
18 st, err := store.Open(":memory:")
19 if err != nil {
20 t.Fatal(err)
21 }
22 defer st.Close()
23 if err := st.MigrateUp(); err != nil {
24 t.Fatal(err)
25 }
26 owner, err := st.CreateUser("alice", false)
27 if err != nil {
28 t.Fatal(err)
29 }
30 reader, err := st.CreateUser("bob", false)
31 if err != nil {
32 t.Fatal(err)
33 }
34 if _, err := st.CreateRepo("user", owner, "app", "public"); err != nil {
35 t.Fatal(err)
36 }
37
38 cfg := config.Default()
39 cfg.Web.Mode = "accounts"
40 s := New(cfg, st)
41
42 // mrs reads the viewer through s.viewer(r), which resolves a
43 // session cookie (internal/httpd/accounts.go:37-47) rather than
44 // taking the viewer as a parameter the way a POST handler test
45 // does. Give a real viewer a real session; leave the request
46 // cookie-less for the anonymous case.
47 sessionFor := func(uid int64) *http.Cookie {
48 tok, hash, err := store.NewToken()
49 if err != nil {
50 t.Fatal(err)
51 }
52 if err := st.CreateWebSession(hash, uid, time.Hour); err != nil {
53 t.Fatal(err)
54 }
55 return s.sessionCookieFor(tok)
56 }
57
58 get := func(uid int64) string {
59 req := httptest.NewRequest("GET", "/alice/app/mrs", nil)
60 req.SetPathValue("owner", "alice")
61 req.SetPathValue("repo", "app")
62 if uid != 0 {
63 req.AddCookie(sessionFor(uid))
64 }
65 rr := httptest.NewRecorder()
66 s.mrs(rr, req)
67 return rr.Body.String()
68 }
69 anonymous := get(0)
70 if !strings.Contains(anonymous, "Sign in to propose a change") {
71 t.Errorf("signed-out visitor: missing sign-in prompt:\n%s", anonymous)
72 }
73 if strings.Contains(anonymous, "New merge request") {
74 t.Error("signed-out visitor should not see New merge request")
75 }
76
77 readerOut := get(reader)
78 if !strings.Contains(readerOut, "Fork this repository to propose a change") {
79 t.Errorf("reader without push: missing fork hint:\n%s", readerOut)
80 }
81
82 ownerOut := get(owner)
83 if !strings.Contains(ownerOut, "New merge request") {
84 t.Errorf("owner: missing New merge request link:\n%s", ownerOut)
85 }
86}
internal/httpd/mrsrow_test.go +7 −6
@@ -10,12 +10,13 @@ import (
10// mrsPageData mirrors the anonymous struct the mrs handler renders with. 10// mrsPageData mirrors the anonymous struct the mrs handler renders with.
11type mrsPageData struct { 11type mrsPageData struct {
12 repoPage 12 repoPage
13 State string 13 State string
14 Query string 14 Query string
15 Filters []listFilter 15 Filters []listFilter
16 Facets []facetGroup 16 Facets []facetGroup
17 MRs []mrRow 17 MRs []mrRow
18 Older string 18 Older string
19 CanWrite bool
19} 20}
20 21
21func renderMRs(t *testing.T, rows []mrRow, state string) string { 22func renderMRs(t *testing.T, rows []mrRow, state string) string {
internal/httpd/searchweb_test.go added +28
@@ -0,0 +1,28 @@
1package httpd
2
3import (
4 "strings"
5 "testing"
6
7 "gitbay.org/gitbay/internal/web"
8)
9
10// The scope sentence is a permanent caption, not a first-visit-only
11// hint: a visitor who has already searched still needs to know what a
12// search here does and does not cover (#270).
13func TestGlobalSearchScopeCaptionAlwaysShown(t *testing.T) {
14 var sb strings.Builder
15 if err := web.Render(&sb, "globalsearch.html", struct {
16 basePage
17 Tab string
18 Query string
19 Kind string
20 QueryErr string
21 Results []searchResult
22 }{basePage{Site: "gitbay"}, "sitesearch", "gitbay", "", "", nil}); err != nil {
23 t.Fatalf("render: %v", err)
24 }
25 if !strings.Contains(sb.String(), "File contents are searched per repository") {
26 t.Error("scope caption missing once a query is present")
27 }
28}
internal/httpd/web.go +3 −1
@@ -2037,6 +2037,7 @@ func (s *Server) mrs(w http.ResponseWriter, r *http.Request) {
2037 return 2037 return
2038 } 2038 }
2039 p.Tab = "merge requests" 2039 p.Tab = "merge requests"
2040 canWrite := s.canWriteRepo(r, p.Repo)
2040 state := r.URL.Query().Get("state") 2041 state := r.URL.Query().Get("state")
2041 if state == "" { 2042 if state == "" {
2042 state = "open" 2043 state = "open"
@@ -2096,9 +2097,10 @@ func (s *Server) mrs(w http.ResponseWriter, r *http.Request) {
2096 MRs []mrRow 2097 MRs []mrRow
2097 LabelColors map[string]template.CSS 2098 LabelColors map[string]template.CSS
2098 Older string 2099 Older string
2100 CanWrite bool
2099 }{p, state, mf.Search, 2101 }{p, state, mf.Search,
2100 activeFilters(state, [][2]string{{"label", mf.Label}, {"author", mf.Author}, {"milestone", mf.Milestone}}), 2102 activeFilters(state, [][2]string{{"label", mf.Label}, {"author", mf.Author}, {"milestone", mf.Milestone}}),
2101 facets, rows, s.labelColors(p.Repo), older}) 2103 facets, rows, s.labelColors(p.Repo), older, canWrite})
2102} 2104}
2103 2105
2104func (s *Server) mr(w http.ResponseWriter, r *http.Request) { 2106func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
internal/web/templates/builds.html +1 −1
@@ -50,7 +50,7 @@
50 {{range .Builds}}<a class="chip check-{{.Status}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/builds/{{.Number}}">{{.Job}}</a>{{end}} 50 {{range .Builds}}<a class="chip check-{{.Status}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/builds/{{.Number}}">{{.Job}}</a>{{end}}
51 </div> 51 </div>
52</li> 52</li>
53{{else}}<li class="empty">no builds — push a commit with a <code>.gitbay/ci.yml</code></li>{{end}} 53{{else}}<li class="empty">no builds{{if .CanWrite}} — push a commit with a .gitbay/ci.yml{{end}}</li>{{end}}
54</ul> 54</ul>
55{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}} 55{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}}
56</div> 56</div>
internal/web/templates/dashboard.html +1 −1
@@ -26,7 +26,7 @@
26 <ul class="pins"> 26 <ul class="pins">
27 {{range .Pins}}<li><a href="/{{.Owner}}/{{.Name}}"><span class="owner">{{.Owner}}/</span>{{.Name}}</a><span class="n" title="{{.Issues}} open issue{{if ne .Issues 1}}s{{end}}, {{.MRs}} open merge request{{if ne .MRs 1}}s{{end}}{{with .Build}}, last build {{.}}{{end}}"><b{{if .Issues}} class="wants"{{end}}>{{.Issues}} <span class="vh">open issues</span></b> <b{{if .MRs}} class="wants"{{end}}>{{.MRs}} <span class="vh">open merge requests</span></b> <span class="dot{{if eq .Build "success"}} ok{{else if or (eq .Build "failure") (eq .Build "error")}} bad{{else if or (eq .Build "pending") (eq .Build "running")}} pend{{end}}"><span class="vh">{{with .Build}}last build {{.}}{{else}}no builds{{end}}</span></span></span></li> 27 {{range .Pins}}<li><a href="/{{.Owner}}/{{.Name}}"><span class="owner">{{.Owner}}/</span>{{.Name}}</a><span class="n" title="{{.Issues}} open issue{{if ne .Issues 1}}s{{end}}, {{.MRs}} open merge request{{if ne .MRs 1}}s{{end}}{{with .Build}}, last build {{.}}{{end}}"><b{{if .Issues}} class="wants"{{end}}>{{.Issues}} <span class="vh">open issues</span></b> <b{{if .MRs}} class="wants"{{end}}>{{.MRs}} <span class="vh">open merge requests</span></b> <span class="dot{{if eq .Build "success"}} ok{{else if or (eq .Build "failure") (eq .Build "error")}} bad{{else if or (eq .Build "pending") (eq .Build "running")}} pend{{end}}"><span class="vh">{{with .Build}}last build {{.}}{{else}}no builds{{end}}</span></span></span></li>
28 {{end}}</ul> 28 {{end}}</ul>
29 {{else}}<p class="none">Nothing pinned yet. Press Pin on a repository.</p>{{end}} 29 {{else}}<p class="none">nothing pinned — press Pin on a repository you visit</p>{{end}}
30</aside> 30</aside>
31 31
32<section class="dashmain"> 32<section class="dashmain">
internal/web/templates/globalsearch.html +1 −2
@@ -43,9 +43,8 @@
43{{/* The count line above already says nothing matched, so this one 43{{/* The count line above already says nothing matched, so this one
44 carries the way out instead of repeating it. */}} 44 carries the way out instead of repeating it. */}}
45{{else}}<p class="empty-note">Try fewer words{{if .Kind}}, <a href="?q={{.Query}}">search everything</a>,{{end}} or <a href="/explore">browse the repositories</a>.</p>{{end}} 45{{else}}<p class="empty-note">Try fewer words{{if .Kind}}, <a href="?q={{.Query}}">search everything</a>,{{end}} or <a href="/explore">browse the repositories</a>.</p>{{end}}
46{{else}}
47<p class="empty-note">Repository names, descriptions and topics, and the title and body of every issue and merge request you can read. File contents are searched per repository, from a repository's Code tab.</p>
48{{end}} 46{{end}}
47<p class="meta">Repository names, descriptions and topics, and the title and body of every issue and merge request you can read. File contents are searched per repository, from a repository's Code tab.</p>
49</div> 48</div>
50</div> 49</div>
51{{end}} 50{{end}}
internal/web/templates/layout.html +5
@@ -66,6 +66,11 @@
66 <a class="button btn" href="/{{.OwnerName}}/{{.Name}}/fork">Fork</a>{{end}} 66 <a class="button btn" href="/{{.OwnerName}}/{{.Name}}/fork">Fork</a>{{end}}
67 </div> 67 </div>
68 {{if eq $top "code"}}{{if field $ "Mirrors"}}<p class="repometa">{{range $i, $m := field $ "Mirrors"}}{{if $i}} · {{end}}{{if eq $m.Direction "push"}}mirrors to{{else}}mirrors from{{end}} <a href="{{$m.URL}}" rel="nofollow">{{$m.Target}}</a>{{if $m.Error}}, <span class="bad">sync error: {{$m.Error}}</span>{{else if $m.Synced}}, synced {{$m.Synced}}{{end}}{{end}}</p>{{end}}{{end}} 68 {{if eq $top "code"}}{{if field $ "Mirrors"}}<p class="repometa">{{range $i, $m := field $ "Mirrors"}}{{if $i}} · {{end}}{{if eq $m.Direction "push"}}mirrors to{{else}}mirrors from{{end}} <a href="{{$m.URL}}" rel="nofollow">{{$m.Target}}</a>{{if $m.Error}}, <span class="bad">sync error: {{$m.Error}}</span>{{else if $m.Synced}}, synced {{$m.Synced}}{{end}}{{end}}</p>{{end}}{{end}}
69 {{/* A tab's own count badge is omitted at zero (an empty <i> reads as
70 stray punctuation next to the tab word); the dashboard pin row's
71 count chip always shows its number, zero included, the same as
72 every other count chip in the app. Two elements, two rules,
73 decided once here (#270). */}}
69 <nav class="tabs" aria-label="Repository"> 74 <nav class="tabs" aria-label="Repository">
70 <a {{if eq $top "code"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}">Code</a> 75 <a {{if eq $top "code"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}">Code</a>
71 <a {{if eq $top "issues"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/issues">Issues{{with field $ "OpenIssues"}}{{if .}} <i>{{.}}</i>{{end}}{{end}}</a> 76 <a {{if eq $top "issues"}}aria-current="page" {{end}}href="/{{.OwnerName}}/{{.Name}}/issues">Issues{{with field $ "OpenIssues"}}{{if .}} <i>{{.}}</i>{{end}}{{end}}</a>
internal/web/templates/mr.html +2 −2
@@ -144,7 +144,7 @@
144 <div class="grp"> 144 <div class="grp">
145 <h2>Reviewers</h2> 145 <h2>Reviewers</h2>
146 {{if .MR.ReviewRequests}}<p class="row">{{range .MR.ReviewRequests}}<a href="/{{.}}">{{.}}</a> {{end}}</p> 146 {{if .MR.ReviewRequests}}<p class="row">{{range .MR.ReviewRequests}}<a href="/{{.}}">{{.}}</a> {{end}}</p>
147 {{else}}<p class="none">nobody yet</p>{{end}} 147 {{else}}<p class="none">no reviewers</p>{{end}}
148 {{if and .CanWrite (or (eq .MR.State "open") (eq .MR.State "source_gone"))}} 148 {{if and .CanWrite (or (eq .MR.State "open") (eq .MR.State "source_gone"))}}
149 <form method="post" action="{{$base}}/review-request" class="actions"> 149 <form method="post" action="{{$base}}/review-request" class="actions">
150 <input type="text" name="add" aria-label="Add reviewers" autocomplete="off" spellcheck="false" placeholder="add, space-separated"> 150 <input type="text" name="add" aria-label="Add reviewers" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
@@ -175,7 +175,7 @@
175 <div class="grp"> 175 <div class="grp">
176 <h2>Labels</h2> 176 <h2>Labels</h2>
177 {{if .MR.Labels}}<p class="row">{{range .MR.Labels}}<a class="chip label" style="{{index $.LabelColors .}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs?label={{.}}">{{.}}</a> {{end}}</p> 177 {{if .MR.Labels}}<p class="row">{{range .MR.Labels}}<a class="chip label" style="{{index $.LabelColors .}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs?label={{.}}">{{.}}</a> {{end}}</p>
178 {{else}}<p class="none">none yet</p>{{end}} 178 {{else}}<p class="none">{{if or (eq .MR.State "merged") (eq .MR.State "closed")}}no labels{{else}}none yet{{end}}</p>{{end}}
179 {{if .CanWrite}} 179 {{if .CanWrite}}
180 <form method="post" action="{{$base}}/label" class="actions"> 180 <form method="post" action="{{$base}}/label" class="actions">
181 <input type="text" name="add" aria-label="Add labels" autocomplete="off" spellcheck="false" placeholder="add, space-separated"> 181 <input type="text" name="add" aria-label="Add labels" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
internal/web/templates/mrs.html +4 −2
@@ -13,7 +13,9 @@
13 </form> 13 </form>
14 {{range .Filters}}<p class="meta">{{.Key}}: {{if eq .Key "label"}}<span class="chip label" style="{{index $.LabelColors .Value}}">{{.Value}}</span>{{else}}<b>{{.Value}}</b>{{end}} <a href="{{.Clear}}">clear</a></p>{{end}} 14 {{range .Filters}}<p class="meta">{{.Key}}: {{if eq .Key "label"}}<span class="chip label" style="{{index $.LabelColors .Value}}">{{.Value}}</span>{{else}}<b>{{.Value}}</b>{{end}} <a href="{{.Clear}}">clear</a></p>{{end}}
15</div> 15</div>
16{{if .Viewer}}<p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs/new">New merge request</a></p>{{end}} 16{{if .CanWrite}}<p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs/new">New merge request</a></p>
17{{else if .Viewer}}<p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/fork">Fork this repository to propose a change</a></p>
18{{else}}<p class="meta"><a href="/login">Sign in to propose a change</a></p>{{end}}
17<ul class="issuelist rows"> 19<ul class="issuelist rows">
18{{range .MRs}}{{$n := .Number}}<li> 20{{range .MRs}}{{$n := .Number}}<li>
19 <div class="issuemain"> 21 <div class="issuemain">
@@ -26,7 +28,7 @@
26 </div> 28 </div>
27</li> 29</li>
28{{else}}{{if .Query}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests matching “{{.Query}}”</li> 30{{else}}{{if .Query}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests matching “{{.Query}}”</li>
29{{else}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests — open one with <code>gitbay mr create {{.Repo.OwnerName}}/{{.Repo.Name}} --source ... --target {{.Repo.DefaultBranch}}</code></li>{{end}}{{end}} 31{{else}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests</li>{{end}}{{end}}
30</ul> 32</ul>
31{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}} 33{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}}
32</div> 34</div>
internal/web/templates/notifications.html +1 −1
@@ -18,6 +18,6 @@
18 </div> 18 </div>
19 {{if not .Read}}<form method="post" action="/notifications" class="inline"><input type="hidden" name="id" value="{{.ID}}"><button type="submit" class="btn">Mark read</button></form>{{end}} 19 {{if not .Read}}<form method="post" action="/notifications" class="inline"><input type="hidden" name="id" value="{{.ID}}"><button type="submit" class="btn">Mark read</button></form>{{end}}
20</li> 20</li>
21{{else}}<li class="empty">{{if .All}}nothing here yet{{else}}nothing unread — <a href="/notifications?all=1">show all</a>{{end}}</li>{{end}} 21{{else}}<li class="empty">{{if .All}}nothing to show{{else}}no unread notifications — <a href="/notifications?all=1">show all</a>{{end}}</li>{{end}}
22</ul> 22</ul>
23{{end}} 23{{end}}