web: interface guidelines fixes !461

merged merged by cmc on 2026-09-23 05:18 UTC · krz/gitbay:web-guidelines into main

22 files changed, +116 −51

Layout: unified · split

e2e/adminusersweb_test.go +4
@@ -57,6 +57,10 @@ func TestAdminUsersWeb(t *testing.T) {
57 } 57 }
58 post(url.Values{"field": {"enable"}, "user": {"alice"}}) 58 post(url.Values{"field": {"enable"}, "user": {"alice"}})
59 post(url.Values{"field": {"promote"}, "user": {"alice"}}) 59 post(url.Values{"field": {"promote"}, "user": {"alice"}})
60 if out, _, _ := inst.ssh(t, rootKey, "", "admin", "user", "show", "alice", "--json"); strings.Contains(out, `"admin":true`) {
61 t.Fatalf("promote without a confirm took: %s", out)
62 }
63 post(url.Values{"field": {"promote"}, "user": {"alice"}, "confirm": {"alice"}})
60 if out, _, _ := inst.ssh(t, rootKey, "", "admin", "user", "show", "alice", "--json"); !strings.Contains(out, `"admin":true`) { 64 if out, _, _ := inst.ssh(t, rootKey, "", "admin", "user", "show", "alice", "--json"); !strings.Contains(out, `"admin":true`) {
61 t.Fatalf("promote did not take: %s", out) 65 t.Fatalf("promote did not take: %s", out)
62 } 66 }
e2e/mrweb_test.go +2 −1
@@ -483,7 +483,8 @@ func TestMRSupersedes(t *testing.T) {
483 } 483 }
484 484
485 alice := inst.login(t, aliceKey) 485 alice := inst.login(t, aliceKey)
486 if status, body := browserPost(t, alice, inst.base()+"/alice/app/mrs/1/close", url.Values{"by": {"2"}}); status != 200 { 486 // The field's placeholder is "!N", so the "!" is accepted.
487 if status, body := browserPost(t, alice, inst.base()+"/alice/app/mrs/1/close", url.Values{"by": {"!2"}}); status != 200 {
487 t.Fatalf("close post: %d\n%s", status, body) 488 t.Fatalf("close post: %d\n%s", status, body)
488 } 489 }
489 490
e2e/orgweb_test.go +33 −2
@@ -85,10 +85,32 @@ func TestOrgManagementWeb(t *testing.T) {
85 } 85 }
86 } 86 }
87 87
88 // Revoking and removing work the same way round. 88 // Revoking and removing need the team's name typed; a bare post
89 // changes nothing.
89 browserPost(t, alice, inst.base()+"/acme", url.Values{ 90 browserPost(t, alice, inst.base()+"/acme", url.Values{
90 "field": {"team-revoke"}, "team": {"builders"}, "repo": {"acme/widget"}, 91 "field": {"team-revoke"}, "team": {"builders"}, "repo": {"acme/widget"},
91 }) 92 })
93 if out, _, _ := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); !strings.Contains(out, `"acme/widget"`) {
94 t.Fatalf("unconfirmed revoke dropped the grant: %s", out)
95 }
96 browserPost(t, alice, inst.base()+"/acme", url.Values{
97 "field": {"team-revoke"}, "team": {"builders"}, "repo": {"acme/widget"}, "confirm": {"builders"},
98 })
99 if out, _, _ := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); strings.Contains(out, `"acme/widget"`) {
100 t.Fatalf("confirmed revoke left the grant: %s", out)
101 }
102 browserPost(t, alice, inst.base()+"/acme", url.Values{
103 "field": {"team-remove"}, "team": {"builders"}, "user": {"bob"},
104 })
105 if out, _, _ := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); !strings.Contains(out, `"bob"`) {
106 t.Fatalf("unconfirmed team remove took bob out: %s", out)
107 }
108 browserPost(t, alice, inst.base()+"/acme", url.Values{
109 "field": {"team-remove"}, "team": {"builders"}, "user": {"bob"}, "confirm": {"builders"},
110 })
111 if out, _, _ := inst.ssh(t, aliceKey, "", "org", "team", "show", "acme", "builders", "--json"); strings.Contains(out, `"bob"`) {
112 t.Fatalf("confirmed team remove left bob in: %s", out)
113 }
92 114
93 // Deleting the team needs its name typed; a bare post is refused and 115 // Deleting the team needs its name typed; a bare post is refused and
94 // the team stays. 116 // the team stays.
@@ -108,9 +130,18 @@ func TestOrgManagementWeb(t *testing.T) {
108 t.Fatalf("team not deleted: exit %d", code) 130 t.Fatalf("team not deleted: exit %d", code)
109 } 131 }
110 132
111 browserPost(t, alice, inst.base()+"/acme", url.Values{ 133 _, body = browserPost(t, alice, inst.base()+"/acme", url.Values{
112 "field": {"member-remove"}, "user": {"bob"}, 134 "field": {"member-remove"}, "user": {"bob"},
113 }) 135 })
136 if !strings.Contains(body, "type bob to confirm") {
137 t.Fatalf("unconfirmed member remove was not refused:\n%s", body)
138 }
139 if members := orgMembers(t, inst, aliceKey); len(members) != 2 {
140 t.Fatalf("member removed without confirmation: %v", members)
141 }
142 browserPost(t, alice, inst.base()+"/acme", url.Values{
143 "field": {"member-remove"}, "user": {"bob"}, "confirm": {"bob"},
144 })
114 if members := orgMembers(t, inst, aliceKey); len(members) != 1 { 145 if members := orgMembers(t, inst, aliceKey); len(members) != 1 {
115 t.Fatalf("member not removed: %v", members) 146 t.Fatalf("member not removed: %v", members)
116 } 147 }
internal/httpd/adminusers.go +1 −1
@@ -105,7 +105,7 @@ func (s *Server) adminUsersSubmit(w http.ResponseWriter, r *http.Request, viewer
105 back("unknown action") 105 back("unknown action")
106 return 106 return
107 } 107 }
108 if verb == "demote" || verb == "disable" { 108 if verb == "promote" || verb == "demote" || verb == "disable" {
109 if ok, msg := confirmed(r, name); !ok { 109 if ok, msg := confirmed(r, name); !ok {
110 back(msg) 110 back(msg)
111 return 111 return
internal/httpd/mractions.go +1 −1
@@ -87,7 +87,7 @@ func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.U
87 87
88func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) { 88func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
89 args := []string{} 89 args := []string{}
90 if by := strings.TrimSpace(r.FormValue("by")); by != "" { 90 if by := strings.TrimPrefix(strings.TrimSpace(r.FormValue("by")), "!"); by != "" {
91 if _, err := strconv.ParseInt(by, 10, 64); err != nil { 91 if _, err := strconv.ParseInt(by, 10, 64); err != nil {
92 s.mrRedirect(w, r, "the superseding request is a number") 92 s.mrRedirect(w, r, "the superseding request is a number")
93 return 93 return
internal/httpd/orgweb.go +12
@@ -78,6 +78,10 @@ func (s *Server) orgSubmit(w http.ResponseWriter, r *http.Request, u store.User)
78 argv = append(argv, "--role", role) 78 argv = append(argv, "--role", role)
79 } 79 }
80 case "member-remove": 80 case "member-remove":
81 if ok, msg := confirmed(r, user); !ok {
82 back(msg)
83 return
84 }
81 argv = []string{"org", "members", "remove", owner, user} 85 argv = []string{"org", "members", "remove", owner, user}
82 case "team-create": 86 case "team-create":
83 argv = []string{"org", "team", "create", owner, team} 87 argv = []string{"org", "team", "create", owner, team}
@@ -90,11 +94,19 @@ func (s *Server) orgSubmit(w http.ResponseWriter, r *http.Request, u store.User)
90 case "team-add": 94 case "team-add":
91 argv = append([]string{"org", "team", "add", owner, team}, strings.Fields(user)...) 95 argv = append([]string{"org", "team", "add", owner, team}, strings.Fields(user)...)
92 case "team-remove": 96 case "team-remove":
97 if ok, msg := confirmed(r, team); !ok {
98 back(msg)
99 return
100 }
93 argv = append([]string{"org", "team", "remove", owner, team}, strings.Fields(user)...) 101 argv = append([]string{"org", "team", "remove", owner, team}, strings.Fields(user)...)
94 case "team-grant": 102 case "team-grant":
95 argv = []string{"org", "team", "grant", owner, team, 103 argv = []string{"org", "team", "grant", owner, team,
96 strings.TrimSpace(r.FormValue("repo")), r.FormValue("role")} 104 strings.TrimSpace(r.FormValue("repo")), r.FormValue("role")}
97 case "team-revoke": 105 case "team-revoke":
106 if ok, msg := confirmed(r, team); !ok {
107 back(msg)
108 return
109 }
98 argv = []string{"org", "team", "revoke", owner, team, strings.TrimSpace(r.FormValue("repo"))} 110 argv = []string{"org", "team", "revoke", owner, team, strings.TrimSpace(r.FormValue("repo"))}
99 default: 111 default:
100 back("unknown form") 112 back("unknown form")
internal/web/static/style.css +6 −6
@@ -523,7 +523,7 @@ nav.tabs a i { font-style: normal; color: var(--muted); margin-left: 4px; }
523.pagehead h1, .listhead h1, .headrow h1, .listhead h2, .headrow h2 { margin: 0; } 523.pagehead h1, .listhead h1, .headrow h1, .listhead h2, .headrow h2 { margin: 0; }
524.pagehead > p, .listhead > p, .headrow > p { margin: 0; } 524.pagehead > p, .listhead > p, .headrow > p { margin: 0; }
525.pagehead .grow, .listhead .spacer, .headrow .spacer { flex: 1; } 525.pagehead .grow, .listhead .spacer, .headrow .spacer { flex: 1; }
526.issuetitle { margin: 0 0 var(--sp-2); font-size: var(--fs-5); } 526.issuetitle { margin: 0 0 var(--sp-2); font-size: var(--fs-5); text-wrap: balance; }
527.issuenumber { color: var(--muted); font-weight: 400; } 527.issuenumber { color: var(--muted); font-weight: 400; }
528.issuemeta { color: var(--muted); font-size: var(--fs-2); margin: 0 0 var(--sp-4); } 528.issuemeta { color: var(--muted); font-size: var(--fs-2); margin: 0 0 var(--sp-4); }
529.commithead p { margin: var(--sp-1) 0; } 529.commithead p { margin: var(--sp-1) 0; }
@@ -905,7 +905,7 @@ table.tree td.lastcommit a {
905table.tree td.lastcommit a:hover { color: var(--link); } 905table.tree td.lastcommit a:hover { color: var(--link); }
906table.tree th.age, table.tree td.age { width: 20%; } 906table.tree th.age, table.tree td.age { width: 20%; }
907td.age, th.age { text-align: right; white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums; } 907td.age, th.age { text-align: right; white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums; }
908td.size, td.mode { color: var(--muted); white-space: nowrap; } 908td.size, td.mode { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
909table.tree th.size, table.tree td.size { text-align: right; } 909table.tree th.size, table.tree td.size { text-align: right; }
910table.refs td.name { width: 100%; } 910table.refs td.name { width: 100%; }
911table.refs td.sha, td.sha { color: var(--muted); } 911table.refs td.sha, td.sha { color: var(--muted); }
@@ -928,8 +928,8 @@ table.keys.nowrap th, table.keys.nowrap td { white-space: nowrap; }
928 The cell is two fixed slots, each right-aligned. A form cannot span 928 The cell is two fixed slots, each right-aligned. A form cannot span
929 table cells, so one column per control — which would let the table 929 table cells, so one column per control — which would let the table
930 align them — is out without form="" plumbing, and the forms hold 930 align them — is out without form="" plumbing, and the forms hold
931 different numbers of controls: only an admin's role form has a 931 different numbers of controls: the enable form and the disabled
932 confirm field, and the enable form has none. Sizing the slot instead 932 row's Promote have no confirm field. Sizing the slot instead
933 of the form is what puts every button in one column and every field 933 of the form is what puts every button in one column and every field
934 in another; right-aligning the cell alone left the disabled row's 934 in another; right-aligning the cell alone left the disabled row's
935 Promote where other rows keep their field. A slot is a field, a gap 935 Promote where other rows keep their field. A slot is a field, a gap
@@ -1486,7 +1486,7 @@ a.memberchip {
1486a.memberchip:hover { text-decoration: none; border-color: var(--link); color: var(--link); } 1486a.memberchip:hover { text-decoration: none; border-color: var(--link); color: var(--link); }
1487a.memberchip .role { color: var(--muted); } 1487a.memberchip .role { color: var(--muted); }
1488 1488
1489.pathbar { display: flex; align-items: center; gap: var(--sp-2) var(--sp-3); margin: 0 0 var(--sp-3); flex-wrap: wrap; font-size: var(--fs-2); } 1489.pathbar { display: flex; align-items: center; gap: var(--sp-2) var(--sp-3); margin: 0 0 var(--sp-3); flex-wrap: wrap; font-size: var(--fs-2); overflow-wrap: anywhere; }
1490.pathbar .spacer { flex: 1; } 1490.pathbar .spacer { flex: 1; }
1491.pathbar .actions { display: flex; align-items: center; gap: var(--sp-2); } 1491.pathbar .actions { display: flex; align-items: center; gap: var(--sp-2); }
1492/* history · blame · raw were 21px tall and "raw" 22px wide (#232) */ 1492/* history · blame · raw were 21px tall and "raw" 22px wide (#232) */
@@ -1608,6 +1608,7 @@ details.refmenu .refdrop {
1608 min-width: 12rem; 1608 min-width: 12rem;
1609 max-height: 20rem; 1609 max-height: 20rem;
1610 overflow-y: auto; 1610 overflow-y: auto;
1611 overscroll-behavior: contain;
1611 background: var(--canvas); 1612 background: var(--canvas);
1612 border: 1px solid var(--line); 1613 border: 1px solid var(--line);
1613 border-radius: var(--r-card); 1614 border-radius: var(--r-card);
@@ -1849,7 +1850,6 @@ svg.icon { vertical-align: -0.125em; }
1849 cell padding and the card clipped the age; a long name wraps (#232) */ 1850 cell padding and the card clipped the age; a long name wraps (#232) */
1850 table.tree td.name { white-space: normal; overflow-wrap: anywhere; } 1851 table.tree td.name { white-space: normal; overflow-wrap: anywhere; }
1851 .clone pre { white-space: pre-wrap; word-break: break-all; } 1852 .clone pre { white-space: pre-wrap; word-break: break-all; }
1852 .pathbar { overflow-wrap: anywhere; }
1853 .readme .cardbody, .code { padding: var(--sp-3); } 1853 .readme .cardbody, .code { padding: var(--sp-3); }
1854 .thread { margin-left: var(--sp-3); } 1854 .thread { margin-left: var(--sp-3); }
1855 .blamehunk { flex-direction: column; gap: 0; } 1855 .blamehunk { flex-direction: column; gap: 0; }
internal/web/templates/account.html +4 −4
@@ -32,7 +32,7 @@
32 <label for="p-website">Website</label> 32 <label for="p-website">Website</label>
33 <input type="text" id="p-website" name="website" value="{{.Profile.Website}}" placeholder="https://example.org"> 33 <input type="text" id="p-website" name="website" value="{{.Profile.Website}}" placeholder="https://example.org">
34 <label for="p-links">Links</label> 34 <label for="p-links">Links</label>
35 <textarea id="p-links" name="links" rows="3" placeholder="one per line: label|https://... or a bare https://... (at most 5)">{{.LinksText}}</textarea> 35 <textarea id="p-links" name="links" rows="3" placeholder="one per line: label|https://… or a bare https://… (at most 5)">{{.LinksText}}</textarea>
36 <span class="btngroup"><button type="submit" class="btn">Save profile</button></span> 36 <span class="btngroup"><button type="submit" class="btn">Save profile</button></span>
37</form> 37</form>
38<h3>About</h3> 38<h3>About</h3>
@@ -65,7 +65,7 @@ commands and push; a <code>git</code> key can only move git data (e.g., CI).</p>
65 <form method="post" action="/settings" class="setform stack"> 65 <form method="post" action="/settings" class="setform stack">
66 <input type="hidden" name="field" value="key-add"> 66 <input type="hidden" name="field" value="key-add">
67 <label for="key">Public key</label> 67 <label for="key">Public key</label>
68 <textarea id="key" name="key" rows="3" required placeholder="ssh-ed25519 AAAA... you@machine"></textarea> 68 <textarea id="key" name="key" rows="3" required spellcheck="false" placeholder="ssh-ed25519 AAAA… you@machine"></textarea>
69 <label for="key-label">Label</label> 69 <label for="key-label">Label</label>
70 <input id="key-label" name="label" maxlength="64" placeholder="defaults to the key's comment"> 70 <input id="key-label" name="label" maxlength="64" placeholder="defaults to the key's comment">
71 <label for="scope">Scope</label> 71 <label for="scope">Scope</label>
@@ -94,13 +94,13 @@ account and where notifications go.</p>
94 <form method="post" action="/settings" class="setform"> 94 <form method="post" action="/settings" class="setform">
95 <input type="hidden" name="field" value="email-add"> 95 <input type="hidden" name="field" value="email-add">
96 <label for="address">Address</label> 96 <label for="address">Address</label>
97 <input type="email" id="address" name="address" required placeholder="you@example.org"> 97 <input type="email" id="address" name="address" required autocomplete="email" placeholder="you@example.org">
98 <button type="submit" class="btn">Send code</button> 98 <button type="submit" class="btn">Send code</button>
99 </form> 99 </form>
100 <form method="post" action="/settings" class="setform"> 100 <form method="post" action="/settings" class="setform">
101 <input type="hidden" name="field" value="email-verify"> 101 <input type="hidden" name="field" value="email-verify">
102 <label for="code">Verification code</label> 102 <label for="code">Verification code</label>
103 <input type="text" id="code" name="code" required placeholder="from the mail"> 103 <input type="text" id="code" name="code" required autocomplete="one-time-code" autocapitalize="none" spellcheck="false" placeholder="from the mail">
104 <button type="submit" class="btn">Verify</button> 104 <button type="submit" class="btn">Verify</button>
105 </form> 105 </form>
106</details> 106</details>
internal/web/templates/adminusers.html +1 −1
@@ -38,7 +38,7 @@
38 <input type="hidden" name="field" value="promote"> 38 <input type="hidden" name="field" value="promote">
39 <input type="hidden" name="user" value="{{.Username}}"> 39 <input type="hidden" name="user" value="{{.Username}}">
40 <input type="hidden" name="state" value="{{$.State}}"> 40 <input type="hidden" name="state" value="{{$.State}}">
41 <button type="submit" class="btn">Promote</button> 41 {{template "confirmfield" .Username}} <button type="submit" class="btn">Promote</button>
42 </form> 42 </form>
43 {{else}} 43 {{else}}
44 {{/* An account that is not active cannot be promoted, but the row 44 {{/* An account that is not active cannot be promoted, but the row
internal/web/templates/builds.html +1 −1
@@ -11,7 +11,7 @@
11 </div>{{end}}{{end}} 11 </div>{{end}}{{end}}
12 <form method="get" class="searchform compact"> 12 <form method="get" class="searchform compact">
13 <label for="ref" class="colhead">Branch</label> 13 <label for="ref" class="colhead">Branch</label>
14 <input type="text" id="ref" name="ref" value="{{.Filter.Ref}}" list="buildrefs"> 14 <input type="text" id="ref" name="ref" value="{{.Filter.Ref}}" list="buildrefs" spellcheck="false">
15 <datalist id="buildrefs">{{range .Refs}}<option value="{{.}}">{{end}}</datalist> 15 <datalist id="buildrefs">{{range .Refs}}<option value="{{.}}">{{end}}</datalist>
16 <input type="hidden" name="status" value="{{.Filter.Status}}"> 16 <input type="hidden" name="status" value="{{.Filter.Status}}">
17 <input type="hidden" name="job" value="{{.Filter.Job}}"> 17 <input type="hidden" name="job" value="{{.Filter.Job}}">
internal/web/templates/fork.html +1 −1
@@ -11,7 +11,7 @@ own.</p>
11 <option value="{{.Viewer}}">{{.Viewer}}</option> 11 <option value="{{.Viewer}}">{{.Viewer}}</option>
12 {{range .Orgs}}<option value="{{.}}"{{if eq . $.Owner}} selected{{end}}>{{.}}</option>{{end}} 12 {{range .Orgs}}<option value="{{.}}"{{if eq . $.Owner}} selected{{end}}>{{.}}</option>{{end}}
13</select></label> 13</select></label>
14<label>/ Name <input name="name" required maxlength="63" pattern="[a-z0-9][a-z0-9._\-]{0,62}" value="{{.Name}}" aria-describedby="forkhint"></label> 14<label>/ Name <input name="name" required maxlength="63" pattern="[a-z0-9][a-z0-9._\-]{0,62}" value="{{.Name}}" aria-describedby="forkhint" autocomplete="off" spellcheck="false"></label>
15<span class="hint" id="forkhint">Defaults to the source name; change it to fork twice into the same owner.</span></p> 15<span class="hint" id="forkhint">Defaults to the source name; change it to fork twice into the same owner.</span></p>
16<p><button type="submit">Create fork</button></p> 16<p><button type="submit">Create fork</button></p>
17</form> 17</form>
internal/web/templates/issue.html +4 −4
@@ -55,8 +55,8 @@
55 {{else}}<p class="none">none yet</p>{{end}} 55 {{else}}<p class="none">none yet</p>{{end}}
56 {{if .CanWrite}} 56 {{if .CanWrite}}
57 <form method="post" action="{{$base}}/label" class="actions"> 57 <form method="post" action="{{$base}}/label" class="actions">
58 <input type="text" name="add" aria-label="Add labels" placeholder="add, space-separated"> 58 <input type="text" name="add" aria-label="Add labels" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
59 <input type="text" name="remove" aria-label="Remove labels" placeholder="remove"> 59 <input type="text" name="remove" aria-label="Remove labels" autocomplete="off" spellcheck="false" placeholder="remove">
60 <button type="submit" class="btn">Apply</button> 60 <button type="submit" class="btn">Apply</button>
61 </form> 61 </form>
62 {{end}} 62 {{end}}
@@ -67,8 +67,8 @@
67 {{else}}<p class="none">nobody yet</p>{{end}} 67 {{else}}<p class="none">nobody yet</p>{{end}}
68 {{if .CanWrite}} 68 {{if .CanWrite}}
69 <form method="post" action="{{$base}}/assign" class="actions"> 69 <form method="post" action="{{$base}}/assign" class="actions">
70 <input type="text" name="add" aria-label="Add assignees" placeholder="add, space-separated"> 70 <input type="text" name="add" aria-label="Add assignees" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
71 <input type="text" name="remove" aria-label="Remove assignees" placeholder="remove"> 71 <input type="text" name="remove" aria-label="Remove assignees" autocomplete="off" spellcheck="false" placeholder="remove">
72 <button type="submit" class="btn">Apply</button> 72 <button type="submit" class="btn">Apply</button>
73 </form> 73 </form>
74 {{end}} 74 {{end}}
internal/web/templates/layout.html +2 −1
@@ -6,6 +6,7 @@
6<title>{{template "title" .}}</title> 6<title>{{template "title" .}}</title>
7<link rel="stylesheet" href="/static/style.css?v={{styleVersion}}"> 7<link rel="stylesheet" href="/static/style.css?v={{styleVersion}}">
8<link rel="icon" href="/favicon.svg" type="image/svg+xml"> 8<link rel="icon" href="/favicon.svg" type="image/svg+xml">
9<meta name="theme-color" content="#000000">
9{{with field . "Feed"}}<link rel="alternate" type="application/atom+xml" href="{{.}}"> 10{{with field . "Feed"}}<link rel="alternate" type="application/atom+xml" href="{{.}}">
10{{end}}</head> 11{{end}}</head>
11<body> 12<body>
@@ -223,7 +224,7 @@
223 224
224{{/* cmtln turns a line number into the link that opens the comment form 225{{/* cmtln turns a line number into the link that opens the comment form
225 on that line. No JavaScript: the anchor travels in the query. */}} 226 on that line. No JavaScript: the anchor travels in the query. */}}
226{{define "cmtln"}}{{if and .Viewer .Base}}<a class="cmt" title="Comment on this line" href="{{.Base}}?view=diff&amp;cpath={{.Path}}&amp;cline={{.N}}&amp;cside={{.Side}}#compose">{{.N}}</a>{{else}}{{.N}}{{end}}{{end}} 227{{define "cmtln"}}{{if and .Viewer .Base}}<a class="cmt" aria-label="Comment on line {{.N}}" title="Comment on this line" href="{{.Base}}?view=diff&amp;cpath={{.Path}}&amp;cline={{.N}}&amp;cside={{.Side}}#compose">{{.N}}</a>{{else}}{{.N}}{{end}}{{end}}
227 228
228{{/* thread renders one review thread with its reply and resolve controls. 229{{/* thread renders one review thread with its reply and resolve controls.
229 Class carries "stale" for threads whose anchor is gone. */}} 230 Class carries "stale" for threads whose anchor is gone. */}}
internal/web/templates/login.html +1 −1
@@ -11,7 +11,7 @@ expires in fifteen minutes.</p>
11{{if .EmailLogin}} 11{{if .EmailLogin}}
12<form method="post" action="/login"> 12<form method="post" action="/login">
13 <div class="field"><label for="identifier">Username or email address</label> 13 <div class="field"><label for="identifier">Username or email address</label>
14 <input type="text" id="identifier" name="identifier" autocomplete="username" required></div> 14 <input type="text" id="identifier" name="identifier" autocomplete="username" autocapitalize="none" spellcheck="false" required></div>
15 <button type="submit">Email me a link</button> 15 <button type="submit">Email me a link</button>
16</form> 16</form>
17<p>Or, from a machine with your registered key:</p> 17<p>Or, from a machine with your registered key:</p>
internal/web/templates/mr.html +5 −5
@@ -107,7 +107,7 @@
107 </form> 107 </form>
108 <form method="post" action="{{$base}}/close" class="actions"> 108 <form method="post" action="{{$base}}/close" class="actions">
109 <label class="vh" for="by">Closed in favour of</label> 109 <label class="vh" for="by">Closed in favour of</label>
110 <input type="text" id="by" name="by" inputmode="numeric" size="4" placeholder="!N"> 110 <input type="text" id="by" name="by" size="4" autocomplete="off" placeholder="!N">
111 <button type="submit" class="danger">Close without merging</button> 111 <button type="submit" class="danger">Close without merging</button>
112 </form> 112 </form>
113 <form method="post" action="{{$base}}/draft" class="actions"> 113 <form method="post" action="{{$base}}/draft" class="actions">
@@ -143,8 +143,8 @@
143 {{else}}<p class="none">nobody yet</p>{{end}} 143 {{else}}<p class="none">nobody yet</p>{{end}}
144 {{if and .CanWrite (or (eq .MR.State "open") (eq .MR.State "source_gone"))}} 144 {{if and .CanWrite (or (eq .MR.State "open") (eq .MR.State "source_gone"))}}
145 <form method="post" action="{{$base}}/review-request" class="actions"> 145 <form method="post" action="{{$base}}/review-request" class="actions">
146 <input type="text" name="add" aria-label="Add reviewers" placeholder="add, space-separated"> 146 <input type="text" name="add" aria-label="Add reviewers" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
147 <input type="text" name="remove" aria-label="Remove reviewers" placeholder="remove"> 147 <input type="text" name="remove" aria-label="Remove reviewers" autocomplete="off" spellcheck="false" placeholder="remove">
148 <button type="submit" class="btn">Apply</button> 148 <button type="submit" class="btn">Apply</button>
149 </form> 149 </form>
150 {{end}} 150 {{end}}
@@ -174,8 +174,8 @@
174 {{else}}<p class="none">none yet</p>{{end}} 174 {{else}}<p class="none">none yet</p>{{end}}
175 {{if .CanWrite}} 175 {{if .CanWrite}}
176 <form method="post" action="{{$base}}/label" class="actions"> 176 <form method="post" action="{{$base}}/label" class="actions">
177 <input type="text" name="add" aria-label="Add labels" placeholder="add, space-separated"> 177 <input type="text" name="add" aria-label="Add labels" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
178 <input type="text" name="remove" aria-label="Remove labels" placeholder="remove"> 178 <input type="text" name="remove" aria-label="Remove labels" autocomplete="off" spellcheck="false" placeholder="remove">
179 <button type="submit" class="btn">Apply</button> 179 <button type="submit" class="btn">Apply</button>
180 </form> 180 </form>
181 {{end}} 181 {{end}}
internal/web/templates/new.html +2 −2
@@ -9,7 +9,7 @@
9 <option value="{{.Viewer}}">{{.Viewer}}</option> 9 <option value="{{.Viewer}}">{{.Viewer}}</option>
10 {{range .Orgs}}<option value="{{.}}">{{.}}</option>{{end}} 10 {{range .Orgs}}<option value="{{.}}">{{.}}</option>{{end}}
11</select></label> 11</select></label>
12<label>/ Name <input name="name" required maxlength="63" pattern="[a-z0-9][a-z0-9._\-]{0,62}" aria-describedby="namehint"></label> 12<label>/ Name <input name="name" required maxlength="63" pattern="[a-z0-9][a-z0-9._\-]{0,62}" aria-describedby="namehint" autocomplete="off" spellcheck="false"></label>
13<span class="hint" id="namehint">Lowercase letters, digits, dot, dash and underscore; must start with a letter or digit; up to 63 characters.</span></p> 13<span class="hint" id="namehint">Lowercase letters, digits, dot, dash and underscore; must start with a letter or digit; up to 63 characters.</span></p>
14<fieldset class="segmented"> 14<fieldset class="segmented">
15 <legend>Visibility</legend> 15 <legend>Visibility</legend>
@@ -26,7 +26,7 @@
26grants access through teams. You are its first admin.</p> 26grants access through teams. You are its first admin.</p>
27<form method="post" action="/new"> 27<form method="post" action="/new">
28<input type="hidden" name="field" value="org-create"> 28<input type="hidden" name="field" value="org-create">
29<p><label>Name <input name="name" required maxlength="63" pattern="[a-z0-9][a-z0-9._\-]{0,62}" aria-describedby="orghint"></label> 29<p><label>Name <input name="name" required maxlength="63" pattern="[a-z0-9][a-z0-9._\-]{0,62}" aria-describedby="orghint" autocomplete="off" spellcheck="false"></label>
30<span class="hint" id="orghint">The same rules as a repository name, and it becomes a top-level path.</span></p> 30<span class="hint" id="orghint">The same rules as a repository name, and it becomes a top-level path.</span></p>
31<p><button type="submit">Create organization</button></p> 31<p><button type="submit">Create organization</button></p>
32</form> 32</form>
internal/web/templates/owner.html +27 −11
@@ -89,7 +89,7 @@
89{{range .Members}}<tr> 89{{range .Members}}<tr>
90 <td><a href="/{{.Name}}">{{.Name}}</a></td> 90 <td><a href="/{{.Name}}">{{.Name}}</a></td>
91 <td>{{.Role}}</td> 91 <td>{{.Role}}</td>
92 <td class="act"><form method="post" action="/{{$org}}"><input type="hidden" name="field" value="member-remove"><input type="hidden" name="user" value="{{.Name}}"><button type="submit" class="linklike">Remove</button></form></td> 92 <td class="act"><form method="post" action="/{{$org}}"><input type="hidden" name="field" value="member-remove"><input type="hidden" name="user" value="{{.Name}}">{{template "confirmfield" .Name}} <button type="submit" class="linklike">Remove</button></form></td>
93</tr> 93</tr>
94{{end}}</table></div> 94{{end}}</table></div>
95<details class="editbox"> 95<details class="editbox">
@@ -97,7 +97,7 @@
97 <form method="post" action="/{{$org}}" class="setform stack"> 97 <form method="post" action="/{{$org}}" class="setform stack">
98 <input type="hidden" name="field" value="member-add"> 98 <input type="hidden" name="field" value="member-add">
99 <label for="member">Username</label> 99 <label for="member">Username</label>
100 <input type="text" id="member" name="user" required> 100 <input type="text" id="member" name="user" required autocomplete="off" spellcheck="false">
101 <label class="none" for="memberrole">Role</label> 101 <label class="none" for="memberrole">Role</label>
102 <select id="memberrole" name="role"> 102 <select id="memberrole" name="role">
103 <option value="member">member</option> 103 <option value="member">member</option>
@@ -118,25 +118,41 @@ of a team get its role on every repository it is granted.</p>
118 <p class="meta">members: {{range .Members}}<a class="memberchip" href="/{{.}}">{{.}}</a> {{else}}<span class="none">none yet</span>{{end}}</p> 118 <p class="meta">members: {{range .Members}}<a class="memberchip" href="/{{.}}">{{.}}</a> {{else}}<span class="none">none yet</span>{{end}}</p>
119 <p class="meta">repositories: {{range .Grants}}<span class="memberchip"><a href="/{{.RepoPath}}">{{.RepoPath}}</a> <span class="role">{{.Role}}</span></span> {{else}}<span class="none">none yet</span>{{end}}</p> 119 <p class="meta">repositories: {{range .Grants}}<span class="memberchip"><a href="/{{.RepoPath}}">{{.RepoPath}}</a> <span class="role">{{.Role}}</span></span> {{else}}<span class="none">none yet</span>{{end}}</p>
120 <form method="post" action="/{{$org}}" class="setform stack"> 120 <form method="post" action="/{{$org}}" class="setform stack">
121 <input type="hidden" name="field" value="team-add">
121 <input type="hidden" name="team" value="{{.Name}}"> 122 <input type="hidden" name="team" value="{{.Name}}">
122 <label class="none" for="tm-{{.Name}}">Members</label> 123 <label class="none" for="tm-{{.Name}}">Add members</label>
123 <input type="text" id="tm-{{.Name}}" name="user" placeholder="usernames, space-separated"> 124 <input type="text" id="tm-{{.Name}}" name="user" placeholder="usernames, space-separated" autocomplete="off" spellcheck="false">
124 <button type="submit" name="field" value="team-add" class="btn">Add</button> 125 <button type="submit" class="btn">Add</button>
125 <button type="submit" name="field" value="team-remove" class="btn">Remove</button>
126 </form> 126 </form>
127 {{if .Members}}<form method="post" action="/{{$org}}" class="setform stack">
128 <input type="hidden" name="field" value="team-remove">
129 <input type="hidden" name="team" value="{{.Name}}">
130 <label class="none" for="tmr-{{.Name}}">Remove members</label>
131 <input type="text" id="tmr-{{.Name}}" name="user" placeholder="usernames, space-separated" autocomplete="off" spellcheck="false">
132 {{template "confirmfield" .Name}}
133 <button type="submit" class="btn">Remove</button>
134 </form>{{end}}
127 <form method="post" action="/{{$org}}" class="setform stack"> 135 <form method="post" action="/{{$org}}" class="setform stack">
136 <input type="hidden" name="field" value="team-grant">
128 <input type="hidden" name="team" value="{{.Name}}"> 137 <input type="hidden" name="team" value="{{.Name}}">
129 <label class="none" for="tr-{{.Name}}">Repository</label> 138 <label class="none" for="tr-{{.Name}}">Grant a repository</label>
130 <input type="text" id="tr-{{.Name}}" name="repo" placeholder="owner/name"> 139 <input type="text" id="tr-{{.Name}}" name="repo" placeholder="owner/name" autocomplete="off" spellcheck="false">
131 <label class="none" for="trr-{{.Name}}">Role</label> 140 <label class="none" for="trr-{{.Name}}">Role</label>
132 <select id="trr-{{.Name}}" name="role"> 141 <select id="trr-{{.Name}}" name="role">
133 <option value="read">read</option> 142 <option value="read">read</option>
134 <option value="write">write</option> 143 <option value="write">write</option>
135 <option value="admin">admin</option> 144 <option value="admin">admin</option>
136 </select> 145 </select>
137 <button type="submit" name="field" value="team-grant" class="btn">Grant</button> 146 <button type="submit" class="btn">Grant</button>
138 <button type="submit" name="field" value="team-revoke" class="btn">Revoke</button>
139 </form> 147 </form>
148 {{if .Grants}}<form method="post" action="/{{$org}}" class="setform stack">
149 <input type="hidden" name="field" value="team-revoke">
150 <input type="hidden" name="team" value="{{.Name}}">
151 <label class="none" for="trv-{{.Name}}">Revoke a repository</label>
152 <input type="text" id="trv-{{.Name}}" name="repo" placeholder="owner/name" autocomplete="off" spellcheck="false">
153 {{template "confirmfield" .Name}}
154 <button type="submit" class="btn">Revoke</button>
155 </form>{{end}}
140 <form method="post" action="/{{$org}}" class="setform"> 156 <form method="post" action="/{{$org}}" class="setform">
141 <input type="hidden" name="field" value="team-delete"> 157 <input type="hidden" name="field" value="team-delete">
142 <input type="hidden" name="team" value="{{.Name}}"> 158 <input type="hidden" name="team" value="{{.Name}}">
@@ -162,7 +178,7 @@ of a team get its role on every repository it is granted.</p>
162 <form method="post" action="/{{$org}}" class="setform"> 178 <form method="post" action="/{{$org}}" class="setform">
163 <input type="hidden" name="field" value="org-rename"> 179 <input type="hidden" name="field" value="org-rename">
164 <label for="orgrename">New name</label> 180 <label for="orgrename">New name</label>
165 <input type="text" id="orgrename" name="name" value="{{$org}}" required> 181 <input type="text" id="orgrename" name="name" value="{{$org}}" required autocomplete="off" spellcheck="false">
166 <button type="submit" class="btn">Rename</button> 182 <button type="submit" class="btn">Rename</button>
167 </form> 183 </form>
168 <p class="meta">Every clone URL under this organization changes with the name.</p> 184 <p class="meta">Every clone URL under this organization changes with the name.</p>
internal/web/templates/refs.html +2 −2
@@ -2,8 +2,8 @@
2{{define "content"}} 2{{define "content"}}
3<h1>Refs</h1> 3<h1>Refs</h1>
4<form method="get" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/compare" class="compareform"> 4<form method="get" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/compare" class="compareform">
5 <label>Compare <input name="base" value="{{.Repo.DefaultBranch}}" size="14" aria-label="base ref"></label> 5 <label>Compare <input name="base" value="{{.Repo.DefaultBranch}}" size="14" aria-label="Compare base ref" autocomplete="off" spellcheck="false"></label>
6 <label>with <input name="head" size="14" placeholder="branch, tag or sha" aria-label="head ref"></label> 6 <label>with <input name="head" size="14" placeholder="branch, tag or sha" aria-label="with head ref" autocomplete="off" spellcheck="false"></label>
7 <button type="submit" class="btn">Compare</button> 7 <button type="submit" class="btn">Compare</button>
8</form> 8</form>
9<h2>Branches</h2> 9<h2>Branches</h2>
internal/web/templates/register.html +4 −4
@@ -6,9 +6,9 @@
6{{else}}<p class="lede">Open registration. Your account activates once you verify your email.</p>{{end}} 6{{else}}<p class="lede">Open registration. Your account activates once you verify your email.</p>{{end}}
7{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}} 7{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
8<form method="post" action="/register" class="signupform"> 8<form method="post" action="/register" class="signupform">
9<div class="field"><label for="username">Username</label><input type="text" id="username" name="username" value="{{.Username}}" required></div> 9<div class="field"><label for="username">Username</label><input type="text" id="username" name="username" value="{{.Username}}" required autocomplete="username" autocapitalize="none" spellcheck="false"></div>
10{{if eq .Mode "invite"}}<div class="field"><label for="invite">Invite code</label><input type="text" id="invite" name="invite" required></div> 10{{if eq .Mode "invite"}}<div class="field"><label for="invite">Invite code</label><input type="text" id="invite" name="invite" required autocomplete="off" spellcheck="false"></div>
11{{else}}<div class="field"><label for="email">Email</label><input type="text" id="email" name="email" required></div>{{end}} 11{{else}}<div class="field"><label for="email">Email</label><input type="email" id="email" name="email" required autocomplete="email"></div>{{end}}
12<div class="field"><label for="key">SSH public key</label> 12<div class="field"><label for="key">SSH public key</label>
13<p class="hint">Paste the contents of your public key file, usually <code>~/.ssh/id_ed25519.pub</code>. It starts with <code>ssh-ed25519</code> or <code>ssh-rsa</code>.</p> 13<p class="hint">Paste the contents of your public key file, usually <code>~/.ssh/id_ed25519.pub</code>. It starts with <code>ssh-ed25519</code> or <code>ssh-rsa</code>.</p>
14{{/* The help is a disclosure rather than a link out: a wiki page is on 14{{/* The help is a disclosure rather than a link out: a wiki page is on
@@ -19,7 +19,7 @@
19<pre class="code" tabindex="0">ssh-keygen -t ed25519 19<pre class="code" tabindex="0">ssh-keygen -t ed25519
20cat ~/.ssh/id_ed25519.pub</pre> 20cat ~/.ssh/id_ed25519.pub</pre>
21</details> 21</details>
22<textarea id="key" name="key" rows="3" required placeholder="ssh-ed25519 AAAA... you@host"></textarea></div> 22<textarea id="key" name="key" rows="3" required spellcheck="false" placeholder="ssh-ed25519 AAAA… you@host"></textarea></div>
23<p><button type="submit">Create account</button></p> 23<p><button type="submit">Create account</button></p>
24</form> 24</form>
25<p class="meta">Prefer the terminal? <code>ssh git@{{.Host}} register --username you {{if eq .Mode "invite"}}--invite &lt;code&gt;{{else}}--email you@example.org{{end}}</code></p> 25<p class="meta">Prefer the terminal? <code>ssh git@{{.Host}} register --username you {{if eq .Mode "invite"}}--invite &lt;code&gt;{{else}}--email you@example.org{{end}}</code></p>
internal/web/templates/settings.html +1 −1
@@ -186,7 +186,7 @@
186 <input type="hidden" name="field" value="runner-add"> 186 <input type="hidden" name="field" value="runner-add">
187 <label for="runner-key">Attach a runner</label> 187 <label for="runner-key">Attach a runner</label>
188 <p class="hint">Install <code>gitbay-runner</code>, run <code>gitbay-runner init</code>, and paste the key it prints. The runner builds your commits with the repository's secrets; merge requests from forks wait unless it runs with <code>-untrusted</code>.</p> 188 <p class="hint">Install <code>gitbay-runner</code>, run <code>gitbay-runner init</code>, and paste the key it prints. The runner builds your commits with the repository's secrets; merge requests from forks wait unless it runs with <code>-untrusted</code>.</p>
189 <textarea id="runner-key" name="key" rows="3" placeholder="ssh-ed25519 AAAA… (from gitbay-runner init)">{{index .Submitted "key"}}</textarea> 189 <textarea id="runner-key" name="key" rows="3" spellcheck="false" placeholder="ssh-ed25519 AAAA… (from gitbay-runner init)">{{index .Submitted "key"}}</textarea>
190 <button type="submit" class="btn">Attach</button> 190 <button type="submit" class="btn">Attach</button>
191</form> 191</form>
192</section> 192</section>
internal/web/templates/snippet.html +1 −1
@@ -29,7 +29,7 @@
29{{if .CanWrite}} 29{{if .CanWrite}}
30<details class="editbox"><summary>add a file</summary> 30<details class="editbox"><summary>add a file</summary>
31<form method="post" action="/{{.Owner}}/-/snippets/{{.Snippet.PublicID}}/file" class="commentform"> 31<form method="post" action="/{{.Owner}}/-/snippets/{{.Snippet.PublicID}}/file" class="commentform">
32<p><input type="text" name="name" aria-label="File name" placeholder="filename" required></p> 32<p><input type="text" name="name" aria-label="File name" placeholder="filename" required autocomplete="off" spellcheck="false"></p>
33<p><textarea name="content" aria-label="Content" rows="12" required></textarea></p> 33<p><textarea name="content" aria-label="Content" rows="12" required></textarea></p>
34<p><button type="submit" class="btn">Add file</button></p> 34<p><button type="submit" class="btn">Add file</button></p>
35</form></details> 35</form></details>
internal/web/templates/snippetnew.html +1 −1
@@ -4,7 +4,7 @@
4<h1>New snippet</h1> 4<h1>New snippet</h1>
5{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}} 5{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
6<form method="post" action="/{{.Owner}}/-/snippets/new" class="commentform"> 6<form method="post" action="/{{.Owner}}/-/snippets/new" class="commentform">
7<p><input type="text" name="name" aria-label="File name" placeholder="filename" value="{{.Name}}" required></p> 7<p><input type="text" name="name" aria-label="File name" placeholder="filename" value="{{.Name}}" required autocomplete="off" spellcheck="false"></p>
8<p><input type="text" name="description" aria-label="Description" placeholder="description" value="{{.Description}}"></p> 8<p><input type="text" name="description" aria-label="Description" placeholder="description" value="{{.Description}}"></p>
9<p><select name="visibility" aria-label="Visibility"> 9<p><select name="visibility" aria-label="Visibility">
10<option value="unlisted"{{if or (eq .Visibility "unlisted") (eq .Visibility "")}} selected{{end}}>unlisted</option> 10<option value="unlisted"{{if or (eq .Visibility "unlisted") (eq .Visibility "")}} selected{{end}}>unlisted</option>