Commit 419f6dfdc5
Verified · cmc ci/build: success ci/test: success
CHANGELOG.org +30 −2
| @@ -19,12 +19,40 @@ The profile's tabs, reworked. | ||
| 19 | 19 | goes back further. |
| 20 | 20 | - Bookmarks are a tab on your own profile. =repo bookmarks= takes no |
| 21 | 21 | owner and lists the caller's, so the tab is not offered on anyone |
| 22 | else's and its URL is a 404 there. =/bookmarks= redirects to it, and | |
| 23 | the rail points at the tab. | |
| 22 | else's and its URL is a 404 there. =/bookmarks= redirects to it. | |
| 24 | 23 | - The snippet list renders inside the profile rather than on a page of |
| 25 | 24 | its own, so it carries the header and the tab bar like every other |
| 26 | 25 | section. A snippet itself is still its own page. |
| 27 | 26 | |
| 27 | Eleven web findings, mostly alignment and affordance (#247). | |
| 28 | ||
| 29 | - Logging out asks first. =GET /logout= is the confirmation page the | |
| 30 | rail's signout square and the More menu link to; the button on it | |
| 31 | posts to the same path, which is still what ends the session. | |
| 32 | - Bookmarks and Snippets are gone from the rail and the More menu. Both | |
| 33 | land on the profile, where they are tabs, and every square left in | |
| 34 | the rail is an instance-level destination. | |
| 35 | - A repository tree marks a directory with a folder glyph and a file | |
| 36 | with a file glyph. The trailing slash still says it in text and the | |
| 37 | glyphs are =aria-hidden=, so the cue no longer rests on the link | |
| 38 | colour alone. Per-filetype icons are deliberately not done. | |
| 39 | - A =/search= hit carries its kind in a chip, in the words =search= | |
| 40 | prints, so the meta line no longer has to say "repository". | |
| 41 | - A settings row lines its control up on the right: a checkbox, a | |
| 42 | number and a full-width text input all end on the same rule, with | |
| 43 | Save in its own column. The label column keeps a 14rem floor so a | |
| 44 | hint does not wrap a word to a line. | |
| 45 | - The email rows on =/settings= put their buttons and confirm field on | |
| 46 | the right, so they line up however long an address is. | |
| 47 | - =/admin/users= draws a disabled Promote on an account that is not | |
| 48 | active, rather than leaving the cell short. | |
| 49 | - The profile's inactive tabs draw their underline in =--line=, so the | |
| 50 | strip reads as a track the current tab is marked in. | |
| 51 | - =.listhead= sheds its children's block margins, which is what kept a | |
| 52 | =p.meta= from sitting level with the heading beside it. | |
| 53 | - =nav.tabs= has a bottom margin. | |
| 54 | - The footer links the iOS client. | |
| 55 | ||
| 28 | 56 | * v1.33.0 — 2026-09-21 |
| 29 | 57 | |
| 30 | 58 | Five findings from the outside review of the web UI (#241–#245). |
e2e/accounts_test.go +10
| @@ -235,6 +235,16 @@ func TestWebAccounts(t *testing.T) { | ||
| 235 | 235 | t.Fatalf("cross-origin POST: %d, want 403", resp.StatusCode) |
| 236 | 236 | } |
| 237 | 237 | |
| 238 | // Logging out is confirmed first: the GET renders the page and leaves | |
| 239 | // the session alone, and only the POST ends it. | |
| 240 | status, body = browserGet(t, browser, inst.base()+"/logout") | |
| 241 | if status != 200 || !strings.Contains(body, `action="/logout"`) { | |
| 242 | t.Fatalf("logout confirmation: %d\n%s", status, body) | |
| 243 | } | |
| 244 | if status, _ = browserGet(t, browser, inst.base()+"/alice/webborn"); status != 200 { | |
| 245 | t.Fatalf("GET /logout ended the session: %d", status) | |
| 246 | } | |
| 247 | ||
| 238 | 248 | // Logout kills the session. |
| 239 | 249 | if status, _ = browserPost(t, browser, inst.base()+"/logout", url.Values{}); status != 200 { |
| 240 | 250 | t.Fatalf("logout: %d", status) |
internal/httpd/accounts.go +9
| @@ -167,6 +167,15 @@ func (s *Server) sessionCookieFor(tok string) *http.Cookie { | ||
| 167 | 167 | } |
| 168 | 168 | } |
| 169 | 169 | |
| 170 | // logoutForm is GET /logout: the confirmation the rail's signout square | |
| 171 | // and the More menu link to, so the session does not end on one stray | |
| 172 | // click. The button posts to the same path. | |
| 173 | func (s *Server) logoutForm(w http.ResponseWriter, r *http.Request, u store.User) { | |
| 174 | s.render(w, "logout.html", struct { | |
| 175 | basePage | |
| 176 | }{s.baseFor(u)}) | |
| 177 | } | |
| 178 | ||
| 170 | 179 | func (s *Server) logout(w http.ResponseWriter, r *http.Request) { |
| 171 | 180 | if ck, err := r.Cookie(sessionCookie); err == nil { |
| 172 | 181 | s.st.DeleteWebSession(store.HashToken(ck.Value)) |
internal/httpd/routes.go +1
| @@ -117,6 +117,7 @@ func (s *Server) Routes() []Route { | ||
| 117 | 117 | Route{Method: "GET", Pattern: "/login", Handler: s.login, Mutating: true}, // consumes a one-time token |
| 118 | 118 | Route{Method: "POST", Pattern: "/login", Mutating: true, |
| 119 | 119 | Handler: s.checkOrigin(s.loginSubmit)}, |
| 120 | Route{Method: "GET", Pattern: "/logout", Handler: s.requireUser(s.logoutForm)}, | |
| 120 | 121 | Route{Method: "POST", Pattern: "/logout", Mutating: true, |
| 121 | 122 | Handler: s.checkOrigin(s.logout)}, |
| 122 | 123 | Route{Method: "GET", Pattern: "/new", Handler: s.requireUser(s.newRepoForm)}, |
internal/web/static/style.css +30 −4
| @@ -480,6 +480,7 @@ nav.tabs { | ||
| 480 | 480 | display: flex; |
| 481 | 481 | gap: var(--sp-1); |
| 482 | 482 | margin-top: var(--sp-3); |
| 483 | margin-bottom: var(--sp-4); | |
| 483 | 484 | overflow-x: auto; |
| 484 | 485 | /* with overflow-y left visible the browser computes it to auto, which |
| 485 | 486 | is a vertical drag on touch; the strip scrolls sideways only */ |
| @@ -500,6 +501,12 @@ nav.tabs a { | ||
| 500 | 501 | } |
| 501 | 502 | nav.tabs a:hover { color: var(--fg); text-decoration: none; } |
| 502 | 503 | nav.tabs a[aria-current] { color: var(--fg); font-weight: 600; border-bottom-color: var(--mark); } |
| 504 | /* the profile strip draws its unlit tabs, so the underline reads as the | |
| 505 | control rather than appearing only on the one you are already on. | |
| 506 | --line, not --faint: the track has to be visible to be a track, and | |
| 507 | at 2px --faint disappears into the canvas in both schemes. */ | |
| 508 | nav.tabs.profiletabs a { border-bottom-color: var(--line); } | |
| 509 | nav.tabs.profiletabs a[aria-current] { border-bottom-color: var(--mark); } | |
| 503 | 510 | /* a count in a tab is plain metadata, not a badge */ |
| 504 | 511 | nav.tabs a i { font-style: normal; color: var(--muted); margin-left: 4px; } |
| 505 | 512 | |
| @@ -511,7 +518,10 @@ nav.tabs a i { font-style: normal; color: var(--muted); margin-left: 4px; } | ||
| 511 | 518 | flex-wrap: wrap; |
| 512 | 519 | margin-bottom: var(--sp-5); |
| 513 | 520 | } |
| 521 | /* every child sheds its block margins, or a p.meta beside an h1 centres | |
| 522 | its text inside a box the margins made taller than the heading's */ | |
| 514 | 523 | .pagehead h1, .listhead h1, .headrow h1, .listhead h2, .headrow h2 { margin: 0; } |
| 524 | .pagehead > p, .listhead > p, .headrow > p { margin: 0; } | |
| 515 | 525 | .pagehead .grow, .listhead .spacer, .headrow .spacer { flex: 1; } |
| 516 | 526 | .issuetitle { margin: 0 0 var(--sp-2); font-size: var(--fs-5); } |
| 517 | 527 | .issuenumber { color: var(--muted); font-weight: 400; } |
| @@ -758,10 +768,15 @@ details.editbox[open] > summary { margin-bottom: var(--sp-2); } | ||
| 758 | 768 | details.editbox input[type="text"] { width: 100%; } |
| 759 | 769 | |
| 760 | 770 | /* ---- settings: one row per control, label then input then its own |
| 761 | button. The name stays setform because the templates carry it. ---- */ | |
| 771 | button. The name stays setform because the templates carry it. | |
| 772 | The label column takes the slack, so the control and the button ride | |
| 773 | the right edge: a checkbox, a number and a full-width text input all | |
| 774 | end on the same rule instead of the narrow ones stranding left. Its | |
| 775 | 14rem floor is what keeps a hint from wrapping a word to a line once | |
| 776 | the control column claims its 28rem. ---- */ | |
| 762 | 777 | form.setform { |
| 763 | 778 | display: grid; |
| 764 | grid-template-columns: 14rem minmax(0, 28rem) auto; | |
| 779 | grid-template-columns: minmax(14rem, 1fr) minmax(0, 28rem) auto; | |
| 765 | 780 | gap: var(--sp-4); |
| 766 | 781 | align-items: start; |
| 767 | 782 | padding: var(--sp-3) 0; |
| @@ -769,7 +784,12 @@ form.setform { | ||
| 769 | 784 | } |
| 770 | 785 | form.setform label { margin-top: 6px; } |
| 771 | 786 | form.setform .hint { margin: 2px 0 0; } |
| 772 | form.setform input[type="text"], form.setform input[type="number"], form.setform select { width: 100%; } | |
| 787 | form.setform input[type="text"], form.setform select { width: 100%; } | |
| 788 | /* a control narrower than its column is pushed to the column's end, | |
| 789 | which is where a full-width input's own right edge lands */ | |
| 790 | form.setform .check { justify-content: flex-end; } | |
| 791 | form.setform .num { text-align: right; } | |
| 792 | form.setform .num input[type="number"] { width: auto; } | |
| 773 | 793 | /* the third column takes the leftover width; the button keeps its own */ |
| 774 | 794 | form.setform > button, form.setform > .btngroup { justify-self: start; } |
| 775 | 795 | form.setform.stack { grid-template-columns: 1fr; } |
| @@ -786,6 +806,9 @@ ul.protlist li:last-child { border-bottom: 0; } | ||
| 786 | 806 | ul.protlist form { margin-left: auto; } |
| 787 | 807 | ul.plain { list-style: none; margin: 0 0 var(--sp-3); padding: 0; } |
| 788 | 808 | ul.plain li { padding: var(--sp-1) 0; display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; } |
| 809 | /* the email list: address and badges read left, the controls sit right, | |
| 810 | so the buttons line up down the list however long an address is */ | |
| 811 | ul.plain li > form:first-of-type { margin-left: auto; } | |
| 789 | 812 | |
| 790 | 813 | /* ---- messages: .error and .notice are one family. The names stay |
| 791 | 814 | because e2e tests match class="error". ---- */ |
| @@ -863,6 +886,9 @@ td.name a { color: var(--fg); } | ||
| 863 | 886 | td.name a:hover { color: var(--link); } |
| 864 | 887 | table.tree td.name { width: 25%; white-space: nowrap; } |
| 865 | 888 | table.tree td.name.dir a { color: var(--link); } |
| 889 | /* the glyph stays muted in both rows: it is a shape cue, and colour here | |
| 890 | goes on carrying what it already carries */ | |
| 891 | table.tree td.name svg { color: var(--muted); vertical-align: -0.125em; margin-right: var(--sp-2); } | |
| 866 | 892 | table.tree th.lastcommit, table.tree td.lastcommit { width: 55%; } |
| 867 | 893 | table.tree td.lastcommit { |
| 868 | 894 | max-width: 0; |
| @@ -1800,7 +1826,7 @@ svg.icon { vertical-align: -0.125em; } | ||
| 1800 | 1826 | ul.loglist .commitside { width: 100%; justify-content: flex-start; } |
| 1801 | 1827 | } |
| 1802 | 1828 | |
| 1803 | /* Eleven 44px squares and the mark are 524px, so below 34rem the rail | |
| 1829 | /* Nine 44px squares and the mark are 440px, so below 34rem the rail | |
| 1804 | 1830 | keeps five — dashboard, search, notifications, More, your avatar — and |
| 1805 | 1831 | the More menu holds the rest. Shrinking the squares instead is what |
| 1806 | 1832 | this replaces: 32px was under the target every phone guideline asks |
internal/web/templates/account.html +3 −3
| @@ -133,21 +133,21 @@ account and where notifications go.</p> | ||
| 133 | 133 | <form method="post" action="/settings" class="setform"> |
| 134 | 134 | <input type="hidden" name="field" value="notify-mail"> |
| 135 | 135 | <label for="notify-mail">Activity by mail</label> |
| 136 | <input type="checkbox" id="notify-mail" name="mail" value="on"{{if .MailOn}} checked{{end}}> | |
| 136 | <div class="check"><input type="checkbox" id="notify-mail" name="mail" value="on"{{if .MailOn}} checked{{end}}></div> | |
| 137 | 137 | <button type="submit" class="btn">Save</button> |
| 138 | 138 | </form> |
| 139 | 139 | <p class="meta">Enable to receive activity alerts by email. Login links will arrive regardless of this setting.</p> |
| 140 | 140 | <form method="post" action="/settings" class="setform"> |
| 141 | 141 | <input type="hidden" name="field" value="notify-watch"> |
| 142 | 142 | <label for="notify-watch">Watch repositories you can write to</label> |
| 143 | <input type="checkbox" id="notify-watch" name="watch" value="on"{{if .WatchOn}} checked{{end}}> | |
| 143 | <div class="check"><input type="checkbox" id="notify-watch" name="watch" value="on"{{if .WatchOn}} checked{{end}}></div> | |
| 144 | 144 | <button type="submit" class="btn">Save</button> |
| 145 | 145 | </form> |
| 146 | 146 | <p class="meta">Alerts for every issue and merge request on those repositories. A watch or mute on a repository has priority over this setting.</p> |
| 147 | 147 | <form method="post" action="/settings" class="setform"> |
| 148 | 148 | <input type="hidden" name="field" value="notify-push"> |
| 149 | 149 | <label for="notify-push">Activity on your registered devices</label> |
| 150 | <input type="checkbox" id="notify-push" name="push" value="on"{{if .PushOn}} checked{{end}}> | |
| 150 | <div class="check"><input type="checkbox" id="notify-push" name="push" value="on"{{if .PushOn}} checked{{end}}></div> | |
| 151 | 151 | <button type="submit" class="btn">Save</button> |
| 152 | 152 | </form> |
| 153 | 153 | <p class="meta">Notification text is sent in full, including for private repositories, so a repository name and item number reach Apple and appear on a lock screen.</p> |
internal/web/templates/adminusers.html +8
| @@ -36,6 +36,14 @@ | ||
| 36 | 36 | <input type="hidden" name="state" value="{{$.State}}"> |
| 37 | 37 | <button type="submit" class="btn">Promote</button> |
| 38 | 38 | </form> |
| 39 | {{else}} | |
| 40 | {{/* An account that is not active cannot be promoted, but the row | |
| 41 | still draws the control so every Actions cell is the same | |
| 42 | shape. Both parts are disabled, so the form never posts. */}} | |
| 43 | <form method="post" action="/admin/users" class="actions" title="Only an active account can be promoted"> | |
| 44 | <input type="text" name="confirm" aria-label="Type {{.Username}} to confirm" placeholder="type {{.Username}} to confirm" size="{{len .Username}}" autocomplete="off" disabled> | |
| 45 | <button type="submit" class="btn" disabled>Promote</button> | |
| 46 | </form> | |
| 39 | 47 | {{end}} |
| 40 | 48 | {{if eq .State "disabled"}} |
| 41 | 49 | <form method="post" action="/admin/users" class="actions"> |
internal/web/templates/globalsearch.html +4 −1
| @@ -31,8 +31,11 @@ | ||
| 31 | 31 | {{range .Results}}<li> |
| 32 | 32 | <div class="issuemain"> |
| 33 | 33 | <p class="title"><a href="{{.Href}}">{{if .Number}}{{.Title}}{{else}}{{.Repo}}{{end}}</a></p> |
| 34 | <p class="meta">{{if .Number}}<a href="/{{.Repo}}">{{.Repo}}</a>{{.Marker}}{{.Number}} opened by <a href="/{{.Author}}">{{.Author}}</a>{{else if .Title}}{{.Title}}{{else}}repository{{end}}</p> | |
| 34 | <p class="meta">{{if .Number}}<a href="/{{.Repo}}">{{.Repo}}</a>{{.Marker}}{{.Number}} opened by <a href="/{{.Author}}">{{.Author}}</a>{{else if .Title}}{{.Title}}{{end}}</p> | |
| 35 | 35 | </div> |
| 36 | {{/* the kind in the words the CLI prints, so a hit reads the same in | |
| 37 | both places; it is why the meta no longer says "repository" */}} | |
| 38 | <span class="chip chip-neutral">{{.Kind}}</span> | |
| 36 | 39 | {{if .State}}<span class="chip chip-{{.State}}">{{.State}}</span>{{end}} |
| 37 | 40 | </li> |
| 38 | 41 | {{end}} |
internal/web/templates/layout.html +5 −7
| @@ -22,8 +22,6 @@ | ||
| 22 | 22 | <li class="railopt">{{template "raillink" dict "Href" "/explore" "Icon" "compass" "Name" "Explore" "Current" (eq (str . "Tab") "explore")}}</li> |
| 23 | 23 | <li>{{template "raillink" dict "Href" "/search" "Icon" "search" "Name" "Search" "Current" (eq (str . "Tab") "sitesearch")}}</li> |
| 24 | 24 | {{if .Viewer}}<li>{{template "raillink" dict "Href" "/notifications" "Icon" "bell" "Name" "Notifications" "Current" (eq (str . "Tab") "notifications") "Count" .Rail.Unread}}</li> |
| 25 | <li class="railopt">{{template "raillink" dict "Href" (printf "/%s/-/bookmarks" .Viewer) "Icon" "bookmark" "Name" "Bookmarks" "Current" (eq (str . "Tab") "bookmarks")}}</li> | |
| 26 | <li class="railopt">{{template "raillink" dict "Href" (printf "/%s/-/snippets" .Viewer) "Icon" "snippet" "Name" "Snippets" "Current" (eq (str . "Tab") "snippets")}}</li> | |
| 27 | 25 | <li class="railopt">{{template "raillink" dict "Href" "/new" "Icon" "plus" "Name" "New repository"}}</li>{{end}} |
| 28 | 26 | </ul> |
| 29 | 27 | <span class="railgap"></span> |
| @@ -36,16 +34,14 @@ | ||
| 36 | 34 | <summary class="railicon" aria-label="More" title="More">{{template "icon" "ellipsis"}}<span class="vh">More</span></summary> |
| 37 | 35 | <div class="raildrop"> |
| 38 | 36 | <a href="/explore">{{template "icon" "compass"}} Explore</a> |
| 39 | <a href="/{{.Viewer}}/-/bookmarks">{{template "icon" "bookmark"}} Bookmarks</a> | |
| 40 | <a href="/{{.Viewer}}/-/snippets">{{template "icon" "snippet"}} Snippets</a> | |
| 41 | 37 | <a href="/new">{{template "icon" "plus"}} New repository</a> |
| 42 | 38 | <a href="/settings">{{template "icon" "gear"}} Settings</a> |
| 43 | 39 | {{if .Admin}}<a href="/admin">{{template "icon" "shield"}} Admin</a>{{end}} |
| 44 | <form method="post" action="/logout"><button type="submit">{{template "icon" "signout"}} Log out</button></form> | |
| 40 | <a href="/logout">{{template "icon" "signout"}} Log out</a> | |
| 45 | 41 | </div> |
| 46 | 42 | </details> |
| 47 | 43 | <a class="railuser" href="/{{.Viewer}}" aria-label="Your profile" title="{{.Viewer}}"><span class="avatar">{{initial .Viewer}}</span></a> |
| 48 | <form class="railopt" method="post" action="/logout"><button type="submit" class="railicon" aria-label="Log out" title="Log out">{{template "icon" "signout"}}<span class="vh">Log out</span></button></form> | |
| 44 | <a class="railopt railicon" href="/logout" aria-label="Log out" title="Log out">{{template "icon" "signout"}}<span class="vh">Log out</span></a> | |
| 49 | 45 | {{else}}<a class="railicon" href="/login" aria-label="Sign in" title="Sign in">{{template "icon" "person"}}<span class="vh">Sign in</span></a>{{end}} |
| 50 | 46 | </div> |
| 51 | 47 | </nav> |
| @@ -88,7 +84,7 @@ | ||
| 88 | 84 | </main> |
| 89 | 85 | |
| 90 | 86 | <footer> |
| 91 | <p>Powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="/privacy">Privacy</a></p> | |
| 87 | <p>Powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="https://gitbay.org/krz/gitbay-ios">iOS app</a> · <a href="/privacy">Privacy</a></p> | |
| 92 | 88 | </footer> |
| 93 | 89 | </div> |
| 94 | 90 | </div> |
| @@ -112,6 +108,8 @@ | ||
| 112 | 108 | {{- else if eq . "ellipsis"}}<circle cx="3.25" cy="8" r="1.1" fill="currentColor" stroke="none"/><circle cx="8" cy="8" r="1.1" fill="currentColor" stroke="none"/><circle cx="12.75" cy="8" r="1.1" fill="currentColor" stroke="none"/> |
| 113 | 109 | {{- else if eq . "snippet"}}<path d="M3.25 2.75h6L12.75 6v7.25a.5.5 0 0 1-.5.5H3.75a.5.5 0 0 1-.5-.5Z"/><path d="M9 2.75V6h3.75"/><path d="m6.75 8.75-1.5 1.5 1.5 1.5M9.75 8.75l1.5 1.5-1.5 1.5"/> |
| 114 | 110 | {{- else if eq . "person"}}<circle cx="8" cy="5.5" r="2.75"/><path d="M2.75 14a5.25 5.25 0 0 1 10.5 0"/> |
| 111 | {{- else if eq . "folder"}}<path d="M1.75 3.25h4l1.5 1.75h6.5v7.5a.5.5 0 0 1-.5.5H2.25a.5.5 0 0 1-.5-.5Z"/> | |
| 112 | {{- else if eq . "file"}}<path d="M3.25 2.75h6L12.75 6v7.25a.5.5 0 0 1-.5.5H3.75a.5.5 0 0 1-.5-.5Z"/><path d="M9 2.75V6h3.75"/> | |
| 115 | 113 | {{- end}}</svg>{{end}} |
| 116 | 114 | |
| 117 | 115 | {{/* raillink draws one square in the rail: the icon, the name for a |
internal/web/templates/logout.html added +9
| @@ -0,0 +1,9 @@ | ||
| 1 | {{define "width"}}bounded{{end}} | |
| 2 | {{define "title"}}log out · {{.Site}}{{end}} | |
| 3 | {{define "content"}} | |
| 4 | <h1>Log out</h1> | |
| 5 | <p class="meta">Ends this browser's session as <a href="/{{.Viewer}}">{{.Viewer}}</a>. Keys and tokens are untouched, and signing back in is a mailed link.</p> | |
| 6 | <form method="post" action="/logout"> | |
| 7 | <p><button type="submit">Log out</button> <a href="/">Cancel</a></p> | |
| 8 | </form> | |
| 9 | {{end}} | |
internal/web/templates/owner.html +1 −1
| @@ -13,7 +13,7 @@ | ||
| 13 | 13 | push the repositories off the bottom of the page (#242). About is |
| 14 | 14 | the bare /{owner}; the rest hang off /-/. A tab nobody may open is |
| 15 | 15 | not offered and its URL is a 404. */}} |
| 16 | <nav class="tabs" aria-label="Profile"> | |
| 16 | <nav class="tabs profiletabs" aria-label="Profile"> | |
| 17 | 17 | <a {{if eq .Tab "about"}}aria-current="page" {{end}}href="/{{.Owner}}">About</a> |
| 18 | 18 | <a {{if eq .Tab "repos"}}aria-current="page" {{end}}href="/{{.Owner}}/-/repositories">Repositories{{if .Repos}} <i>{{len .Repos}}</i>{{end}}</a> |
| 19 | 19 | {{if .Self}}<a {{if eq .Tab "bookmarks"}}aria-current="page" {{end}}href="/{{.Owner}}/-/bookmarks">Bookmarks</a>{{end}} |
internal/web/templates/settings.html +1 −1
| @@ -79,7 +79,7 @@ | ||
| 79 | 79 | <form method="post" action="{{$base}}" class="setform"> |
| 80 | 80 | <input type="hidden" name="field" value="require-approvals"> |
| 81 | 81 | <div><label for="approvals">Approvals</label><p class="hint">Approvals from anyone with write access. Zero means none required.</p></div> |
| 82 | <div><input type="number" id="approvals" name="approvals" min="0" max="10" value="{{.Repo.Settings.RequireApprovals}}"></div> | |
| 82 | <div class="num"><input type="number" id="approvals" name="approvals" min="0" max="10" value="{{.Repo.Settings.RequireApprovals}}"></div> | |
| 83 | 83 | <div><button type="submit" class="btn">Save</button></div> |
| 84 | 84 | </form> |
| 85 | 85 | <form method="post" action="{{$base}}" class="setform"> |
internal/web/templates/tree.html +5 −2
| @@ -19,8 +19,11 @@ | ||
| 19 | 19 | {{with .Tip}}{{if .SHA}}<tr class="tipbar"><td colspan="3"><div class="tip"><span class="who">{{template "authorname" dict "Name" .Author "User" .User "Email" .Email}}</span> <a class="subject" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a><span class="spacer"></span><a class="sha" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}"><code>{{short .SHA}}</code></a> <span class="age"{{if not .When.IsZero}} title="{{whenT .When}}"{{end}}>{{ago .When}}</span></div></td></tr>{{end}}{{end}} |
| 20 | 20 | <tr class="cols"><th scope="col">name</th><th scope="col" class="lastcommit">last commit</th><th scope="col" class="age">updated</th></tr> |
| 21 | 21 | {{range .Entries}}{{$c := index $.LastCommits .Name}}<tr> |
| 22 | {{if eq .Type "tree"}}<td class="name dir"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{.Name}}/</a></td> | |
| 23 | {{else}}<td class="name"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/blob/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{.Name}}</a></td>{{end}} | |
| 22 | {{/* the glyph is decorative: the trailing slash is what says directory | |
| 23 | to anything reading the text, and the icon repeats it in shape so | |
| 24 | the cue does not rest on the link colour alone */}} | |
| 25 | {{if eq .Type "tree"}}<td class="name dir"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{template "icon" "folder"}}{{.Name}}/</a></td> | |
| 26 | {{else}}<td class="name"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/blob/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{template "icon" "file"}}{{.Name}}</a></td>{{end}} | |
| 24 | 27 | <td class="lastcommit">{{with $c.Subject}}<a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{$c.SHA}}" title="{{.}}">{{.}}</a>{{end}}</td> |
| 25 | 28 | <td class="age"{{if not $c.When.IsZero}} title="{{whenT $c.When}}"{{end}}>{{ago $c.When}}</td> |
| 26 | 29 | </tr> |
internal/web/web_test.go +13 −18
| @@ -90,7 +90,7 @@ func TestWhenNamesTheZone(t *testing.T) { | ||
| 90 | 90 | // a per-view define instead of a fixed one. |
| 91 | 91 | func TestMainWidthClass(t *testing.T) { |
| 92 | 92 | wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true, "edit.html": true, "dashboard.html": true, "issues.html": true, "mrs.html": true, "explore.html": true, "notifications.html": true, "settings.html": true, "account.html": true, "admin.html": true} |
| 93 | bounded := map[string]bool{"landing.html": true, "fork.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "adminusers.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true} | |
| 93 | bounded := map[string]bool{"landing.html": true, "fork.html": true, "login.html": true, "logout.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "adminusers.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true} | |
| 94 | 94 | perView := map[string]string{"mr.html": `{{define "width"}}{{if eq .View "diff"}}wide{{else}}reading{{end}}{{end}}`} |
| 95 | 95 | for _, name := range Pages() { |
| 96 | 96 | src, err := TemplateSource(name) |
| @@ -160,7 +160,13 @@ func TestRailIconsAreLabelled(t *testing.T) { | ||
| 160 | 160 | } |
| 161 | 161 | } |
| 162 | 162 | |
| 163 | tagRe := regexp.MustCompile(`(?s)<a class="railicon".*?</a>|<summary class="railicon".*?</summary>|<button [^>]*class="railicon".*?</button>`) | |
| 163 | // Every railicon control in the file, wherever it sits and whatever | |
| 164 | // else its class carries — the strip, the More summary, and the | |
| 165 | // foot's Log out and Sign in. Log out used to be a button and had a | |
| 166 | // check of its own here; it is a link to the confirmation page now, | |
| 167 | // and this loop covers the foot either way. The More menu's own rows | |
| 168 | // are not icon controls — their visible text is their name. | |
| 169 | tagRe := regexp.MustCompile(`(?s)<a [^>]*class="[^"]*railicon.*?</a>|<summary [^>]*class="[^"]*railicon.*?</summary>|<button [^>]*class="[^"]*railicon.*?</button>`) | |
| 164 | 170 | controls := tagRe.FindAllString(src, -1) |
| 165 | 171 | if len(controls) < 3 { |
| 166 | 172 | t.Fatalf("found %d railicon controls in layout.html, want the rail's full set", len(controls)) |
| @@ -176,24 +182,13 @@ func TestRailIconsAreLabelled(t *testing.T) { | ||
| 176 | 182 | } |
| 177 | 183 | } |
| 178 | 184 | |
| 179 | // An icon button in the rail's foot is under the same rule: Log out | |
| 180 | // is the only one today. The More menu's own rows are not icon | |
| 181 | // buttons — their visible text is their name. | |
| 185 | // The foot holds controls of its own rather than raillink squares, so | |
| 186 | // check it is in what tagRe just scanned: a foot written with none | |
| 187 | // would pass the loop above by being empty. | |
| 182 | 188 | foot := src[strings.Index(src, `<div class="railfoot">`):] |
| 183 | 189 | foot = foot[:strings.Index(foot, "</nav>")] |
| 184 | buttons := regexp.MustCompile(`(?s)<button [^>]*class="railicon".*?</button>`).FindAllString(foot, -1) | |
| 185 | if len(buttons) == 0 { | |
| 186 | t.Fatal("no icon button in the rail foot") | |
| 187 | } | |
| 188 | for _, b := range buttons { | |
| 189 | for _, want := range []string{`aria-label="`, `<span class="vh">`} { | |
| 190 | if !strings.Contains(b, want) { | |
| 191 | t.Errorf("rail foot button missing %s: %.80s", want, b) | |
| 192 | } | |
| 193 | } | |
| 194 | if !glyph(b) { | |
| 195 | t.Errorf("rail foot button draws no glyph: %.80s", b) | |
| 196 | } | |
| 190 | if len(tagRe.FindAllString(foot, -1)) == 0 { | |
| 191 | t.Error("no railicon control in the rail foot") | |
| 197 | 192 | } |
| 198 | 193 | |
| 199 | 194 | // No decorative graphic anywhere in the layout is exposed, the brand |