web: one register for empty states and contribution hints !501
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 | |||
| 9 | Credentials and sessions: revocation, delegation, expiry and an idle | 15 | Credentials and sessions: revocation, delegation, expiry and an idle |
| 10 | timeout (#256, #257, #276, #277). | 16 | timeout (#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). | ||
| 63 | func 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). | ||
| 94 | func 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 | |||
| 60 | func TestBuildPageRendersCommandOutput(t *testing.T) { | 123 | func 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). | ||
| 88 | func 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). | ||
| 102 | func 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). | ||
| 119 | func 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 | |||
| 131 | func 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 @@ | |||
| 1 | package httpd | ||
| 2 | |||
| 3 | import ( | ||
| 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). | ||
| 17 | func 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. |
| 11 | type mrsPageData struct { | 11 | type 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 | ||
| 21 | func renderMRs(t *testing.T, rows []mrRow, state string) string { | 22 | func renderMRs(t *testing.T, rows []mrRow, state string) string { |
internal/httpd/searchweb_test.go added +28
| @@ -0,0 +1,28 @@ | |||
| 1 | package httpd | ||
| 2 | |||
| 3 | import ( | ||
| 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). | ||
| 13 | func 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 | ||
| 2104 | func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | 2106 | func (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}} |