Commit 0f51fba689
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
Layout: unified · split
.gitbay/wiki/Parity.org +4 −2
| @@ -269,7 +269,8 @@ missing UI rather than a rule. | |||
| 269 | | teams create, delete | yes | yes | yes | | 269 | | teams create, delete | yes | yes | yes | |
| 270 | | team members | yes | yes | yes | | 270 | | team members | yes | yes | yes | |
| 271 | | team repo grants | yes | yes | yes | | 271 | | team repo grants | yes | yes | yes | |
| 272 | | create, rename, delete | yes | no | no | | 272 | | create, rename | yes | yes | no | |
| 273 | | delete | yes | n/a | n/a | | ||
| 273 | 274 | ||
| 274 | * Pagination | 275 | * Pagination |
| 275 | 276 | ||
| @@ -283,7 +284,8 @@ with the same cursors; iOS pages with them too. | |||
| 283 | 284 | ||
| 284 | Build secrets, mirror configuration and tokens, custom domain claims, | 285 | Build secrets, mirror configuration and tokens, custom domain claims, |
| 285 | API token minting, deploy keys, account and instance administration. Deleting or transferring a repository is also | 286 | API token minting, deploy keys, account and instance administration. Deleting or transferring a repository is also |
| 286 | CLI-only: both want a typed confirmation, not a button. | 287 | CLI-only, as is deleting an organization: each wants a typed |
| 288 | confirmation, not a button. | ||
| 287 | 289 | ||
| 288 | These are the only rows where a =no= is intended. Everywhere else a | 290 | These are the only rows where a =no= is intended. Everywhere else a |
| 289 | =no= is work outstanding, and =n/a= means a surface cannot usefully | 291 | =no= is work outstanding, and =n/a= means a surface cannot usefully |
e2e/orgweb_test.go +55
| @@ -130,3 +130,58 @@ func orgMembers(t *testing.T, inst *instance, key string) []string { | |||
| 130 | } | 130 | } |
| 131 | return names | 131 | return names |
| 132 | } | 132 | } |
| 133 | |||
| 134 | // The organization lifecycle from a browser: create from your own page, | ||
| 135 | // rename from the org's. Delete stays on the CLI, where a typed | ||
| 136 | // confirmation is the norm (#167). | ||
| 137 | func TestOrgLifecycleWeb(t *testing.T) { | ||
| 138 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") | ||
| 139 | aliceKey := inst.newKey(t, "alice") | ||
| 140 | bobKey := inst.newKey(t, "bob") | ||
| 141 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub") | ||
| 142 | inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub") | ||
| 143 | alice := loginBrowser(t, inst, aliceKey) | ||
| 144 | bob := loginBrowser(t, inst, bobKey) | ||
| 145 | |||
| 146 | // The create form is on your own page and nobody else's. | ||
| 147 | if _, body := browserGet(t, alice, inst.base()+"/alice"); !strings.Contains(body, `value="org-create"`) { | ||
| 148 | t.Fatalf("no create form on your own page:\n%s", body) | ||
| 149 | } | ||
| 150 | if _, body := browserGet(t, bob, inst.base()+"/alice"); strings.Contains(body, `value="org-create"`) { | ||
| 151 | t.Fatal("create form on someone else's page") | ||
| 152 | } | ||
| 153 | |||
| 154 | if status, _ := browserPost(t, alice, inst.base()+"/alice", url.Values{ | ||
| 155 | "field": {"org-create"}, "name": {"acmeco"}}); status != 200 { | ||
| 156 | t.Fatal("org create failed") | ||
| 157 | } | ||
| 158 | if out, _, _ := inst.ssh(t, aliceKey, "", "org", "list", "--json"); !strings.Contains(out, "acmeco") { | ||
| 159 | t.Fatalf("org not created:\n%s", out) | ||
| 160 | } | ||
| 161 | |||
| 162 | // Rename is offered to its admin, and the org moves. | ||
| 163 | _, body := browserGet(t, alice, inst.base()+"/acmeco") | ||
| 164 | if !strings.Contains(body, `value="org-rename"`) { | ||
| 165 | t.Fatalf("no rename form for the org admin:\n%s", body) | ||
| 166 | } | ||
| 167 | if !strings.Contains(body, "gitbay org delete") || strings.Contains(body, `value="org-delete"`) { | ||
| 168 | t.Error("delete is not recorded as a CLI operation") | ||
| 169 | } | ||
| 170 | if status, _ := browserPost(t, alice, inst.base()+"/acmeco", url.Values{ | ||
| 171 | "field": {"org-rename"}, "name": {"acmeltd"}}); status != 200 { | ||
| 172 | t.Fatal("org rename failed") | ||
| 173 | } | ||
| 174 | if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 { | ||
| 175 | t.Fatal("renamed org not found under its new name") | ||
| 176 | } | ||
| 177 | if status, _ := browserGet(t, alice, inst.base()+"/acmeco"); status != http.StatusNotFound { | ||
| 178 | t.Errorf("old org name still resolves: %d", status) | ||
| 179 | } | ||
| 180 | |||
| 181 | // A non-admin cannot rename it, form or no form. | ||
| 182 | browserPost(t, bob, inst.base()+"/acmeltd", url.Values{ | ||
| 183 | "field": {"org-rename"}, "name": {"bobsltd"}}) | ||
| 184 | if _, _, code := inst.ssh(t, aliceKey, "", "org", "show", "acmeltd"); code != 0 { | ||
| 185 | t.Fatal("a non-admin renamed the organization") | ||
| 186 | } | ||
| 187 | } | ||
internal/httpd/orgweb.go +17
| @@ -51,6 +51,23 @@ func (s *Server) orgSubmit(w http.ResponseWriter, r *http.Request, u store.User) | |||
| 51 | team := strings.TrimSpace(r.FormValue("team")) | 51 | team := strings.TrimSpace(r.FormValue("team")) |
| 52 | user := strings.TrimSpace(r.FormValue("user")) | 52 | user := strings.TrimSpace(r.FormValue("user")) |
| 53 | 53 | ||
| 54 | // Create and rename land on a different page than the one they were | ||
| 55 | // posted from: a new org has no page yet, and a renamed one has moved. | ||
| 56 | switch field { | ||
| 57 | case "org-create", "org-rename": | ||
| 58 | name := strings.TrimSpace(r.FormValue("name")) | ||
| 59 | argv := []string{"org", "create", name} | ||
| 60 | if field == "org-rename" { | ||
| 61 | argv = []string{"org", "rename", owner, name} | ||
| 62 | } | ||
| 63 | if _, msg, ok := s.runControl(u, argv); !ok { | ||
| 64 | back(msg) | ||
| 65 | return | ||
| 66 | } | ||
| 67 | http.Redirect(w, r, "/"+name, http.StatusSeeOther) | ||
| 68 | return | ||
| 69 | } | ||
| 70 | |||
| 54 | var argv []string | 71 | var argv []string |
| 55 | switch field { | 72 | switch field { |
| 56 | case "member-add": | 73 | case "member-add": |
internal/httpd/web.go +4 −1
| @@ -440,10 +440,13 @@ func (s *Server) ownerPage(w http.ResponseWriter, r *http.Request) { | |||
| 440 | ActivityTotal int | 440 | ActivityTotal int |
| 441 | Teams []teamView | 441 | Teams []teamView |
| 442 | CanAdmin bool | 442 | CanAdmin bool |
| 443 | Self bool | ||
| 443 | Notice string | 444 | Notice string |
| 444 | }{s.baseFor(viewer), name, d.Kind, profile, aboutHTML(profile), | 445 | }{s.baseFor(viewer), name, d.Kind, profile, aboutHTML(profile), |
| 445 | d.Repos, d.Members, d.Orgs, | 446 | d.Repos, d.Members, d.Orgs, |
| 446 | weeks, activityTotal, teams, canAdmin, s.takeFlash(w, r)}) | 447 | weeks, activityTotal, teams, canAdmin, |
| 448 | d.Kind == "user" && viewer.ID != 0 && strings.EqualFold(viewer.Username, name), | ||
| 449 | s.takeFlash(w, r)}) | ||
| 447 | } | 450 | } |
| 448 | 451 | ||
| 449 | func (s *Server) repoHome(w http.ResponseWriter, r *http.Request) { | 452 | func (s *Server) repoHome(w http.ResponseWriter, r *http.Request) { |
internal/web/templates/owner.html +29
| @@ -23,6 +23,20 @@ | |||
| 23 | {{else}}<li class="empty">no visible repositories</li>{{end}} | 23 | {{else}}<li class="empty">no visible repositories</li>{{end}} |
| 24 | </ul> | 24 | </ul> |
| 25 | 25 | ||
| 26 | {{if .Self}} | ||
| 27 | <h2>organizations</h2> | ||
| 28 | {{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}} | ||
| 29 | <details class="editbox"> | ||
| 30 | <summary>New organization</summary> | ||
| 31 | <form method="post" action="/{{.Owner}}" class="setform"> | ||
| 32 | <input type="hidden" name="field" value="org-create"> | ||
| 33 | <label for="orgname">Name</label> | ||
| 34 | <input type="text" id="orgname" name="name" required> | ||
| 35 | <button type="submit">Create</button> | ||
| 36 | </form> | ||
| 37 | </details> | ||
| 38 | {{end}} | ||
| 39 | |||
| 26 | {{if .CanAdmin}}{{$org := .Owner}} | 40 | {{if .CanAdmin}}{{$org := .Owner}} |
| 27 | <h2>people <span class="count">{{len .Members}}</span></h2> | 41 | <h2>people <span class="count">{{len .Members}}</span></h2> |
| 28 | {{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}} | 42 | {{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}} |
| @@ -96,5 +110,20 @@ of a team get its role on every repository it is granted.</p> | |||
| 96 | <button type="submit">Create</button> | 110 | <button type="submit">Create</button> |
| 97 | </form> | 111 | </form> |
| 98 | </details> | 112 | </details> |
| 113 | |||
| 114 | <h2>organization</h2> | ||
| 115 | <details class="editbox"> | ||
| 116 | <summary>Rename</summary> | ||
| 117 | <form method="post" action="/{{$org}}" class="setform"> | ||
| 118 | <input type="hidden" name="field" value="org-rename"> | ||
| 119 | <label for="orgrename">New name</label> | ||
| 120 | <input type="text" id="orgrename" name="name" value="{{$org}}" required> | ||
| 121 | <button type="submit">Rename</button> | ||
| 122 | </form> | ||
| 123 | <p class="meta">Every clone URL under this organization changes with the name.</p> | ||
| 124 | </details> | ||
| 125 | <p class="meta">Deleting an organization is a CLI operation, like deleting a | ||
| 126 | repository: it wants a typed confirmation rather than a button.</p> | ||
| 127 | <pre class="message">gitbay org delete {{$org}} --yes</pre> | ||
| 99 | {{end}} | 128 | {{end}} |
| 100 | {{end}} | 129 | {{end}} |