refactor(profile): pin from repository settings
7 files changed, +26 −34
- flow_test.go modified +12 −5
- static/style.css modified +1 −5
- templates/org.html modified +1 −1
- templates/reposettings.html modified +1 −0
- templates/repositories.html modified +1 −2
- templates/user.html modified +1 −1
- web.go modified +9 −20
flow_test.go modified
| @@ -1,7 +1,6 @@ | ||
| 1 | 1 | package main |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | - "fmt" | |
| 5 | 4 | "net/http" |
| 6 | 5 | "net/http/httptest" |
| 7 | 6 | "net/url" |
| @@ -281,16 +280,20 @@ func TestPins(t *testing.T) { | ||
| 281 | 280 | } |
| 282 | 281 | own, _ := s.store.CreateSession(alice.ID) |
| 283 | 282 | other, _ := s.store.CreateSession(mallory.ID) |
| 283 | + names := map[int64]string{} | |
| 284 | + for i, id := range ids { | |
| 285 | + names[id] = string(rune('a' + i)) | |
| 286 | + } | |
| 284 | 287 | pin := func(id int64, cookie string) *httptest.ResponseRecorder { |
| 285 | - return post(h, "/~alice/repositories", url.Values{"repo": {fmt.Sprint(id)}, "action": {"pin"}}, cookie) | |
| 288 | + return post(h, "/~alice/"+names[id]+"/settings", url.Values{"visibility": {"public"}, "pinned": {"on"}}, cookie) | |
| 286 | 289 | } |
| 287 | - if w := pin(ids[0], other); w.Code != 404 { | |
| 288 | - t.Errorf("someone else pinned alice's repo: %d", w.Code) | |
| 290 | + if pin(ids[0], other); len(s.store.Pins(alice.ID)) != 0 { | |
| 291 | + t.Error("someone else pinned alice's repo") | |
| 289 | 292 | } |
| 290 | 293 | pin(ids[2], own) |
| 291 | 294 | pin(ids[0], own) |
| 292 | 295 | body := get(h, "/~alice", "").Body.String() |
| 293 | - if !strings.Contains(body, "Pinned") || strings.Index(body, `href="/~alice/c"`) > strings.Index(body, `href="/~alice/a"`) || strings.Contains(body, `href="/~alice/b"`) { | |
| 296 | + if !strings.Contains(body, `href="/~alice/c"`) || strings.Index(body, `href="/~alice/c"`) > strings.Index(body, `href="/~alice/a"`) || strings.Contains(body, `href="/~alice/b"`) { | |
| 294 | 297 | t.Error("profile does not show exactly the pinned repos in pin order") |
| 295 | 298 | } |
| 296 | 299 | for _, id := range ids[1:] { |
| @@ -299,6 +302,10 @@ func TestPins(t *testing.T) { | ||
| 299 | 302 | if n := len(s.store.Pins(alice.ID)); n != maxPins { |
| 300 | 303 | t.Errorf("%d pins, want the cap of %d", n, maxPins) |
| 301 | 304 | } |
| 305 | + post(h, "/~alice/a/settings", url.Values{"visibility": {"public"}}, own) | |
| 306 | + if s.store.Pins(alice.ID)[ids[0]] != 0 { | |
| 307 | + t.Error("unticking the box did not unpin") | |
| 308 | + } | |
| 302 | 309 | if w := get(h, "/~alice/repositories", ""); w.Code != 200 || !strings.Contains(w.Body.String(), `href="/~alice/g"`) { |
| 303 | 310 | t.Error("repositories page does not list every repo") |
| 304 | 311 | } |
static/style.css modified
| @@ -187,7 +187,7 @@ input[type=file]{border:0;padding:0} | ||
| 187 | 187 | .twocol{display:grid;grid-template-columns:1fr 1fr;gap:2rem;margin-bottom:2rem} |
| 188 | 188 | .twocol label{display:flex;flex-direction:column;gap:.25rem;margin-bottom:.8rem} |
| 189 | 189 | .twocol .legend{display:block;margin-bottom:.4rem} |
| 190 | -.twocol label.radio,.modes label.radio{flex-direction:row;flex-wrap:wrap;align-items:center;gap:.4rem;margin-bottom:.6rem} | |
| 190 | +.twocol label.radio,.twocol label.check,.modes label.radio{flex-direction:row;flex-wrap:wrap;align-items:center;gap:.4rem;margin-bottom:.6rem} | |
| 191 | 191 | .twocol label.radio small,.modes label.radio small{flex-basis:100%;color:var(--muted);padding-left:1.4rem} |
| 192 | 192 | .modes input[type=radio]{width:auto;margin:0} |
| 193 | 193 | h3.plain{border:0;font-size:1.25rem} |
| @@ -436,7 +436,3 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere} | ||
| 436 | 436 | form.inline{display:inline-flex;align-items:center;gap:.4rem;margin:0 0 .6rem} |
| 437 | 437 | table.keys form.inline{margin:0} |
| 438 | 438 | input.rank{width:4rem} |
| 439 | -.pinhead{display:flex;align-items:baseline;justify-content:space-between;font-weight:400;color:var(--muted);margin:1rem 0 .5rem} | |
| 440 | -.pinhead a{font-size:.9rem;color:var(--accent)} | |
| 441 | -.event h4 .pinform{margin:0 0 0 auto} | |
| 442 | -.event h4 .starcount~.pinform{margin-left:.6rem} | |
templates/org.html modified
| @@ -18,7 +18,7 @@ | ||
| 18 | 18 | <div class="columns orgbody"> |
| 19 | 19 | <div class="main"> |
| 20 | 20 | {{with .Data.Activity}}<section class="activity"><div class="heathead"><p class="muted small">{{.Total}} commit{{if ne .Total 1}}s{{end}} across the organisation {{if .Year}}in {{.Year}}{{else}}in the past 12 months{{end}}</p><nav class="years"><a href="?"{{if not .Year}} class="active"{{end}}>past 12 months</a>{{range .Years}}<a href="?year={{.}}"{{if eq . $.Data.Activity.Year}} class="active"{{end}}>{{.}}</a>{{end}}</nav></div>{{.Grid}}<p class="legend muted small">less <i class="h0"></i><i class="h1"></i><i class="h2"></i><i class="h3"></i><i class="h4"></i> more</p></section>{{end}} |
| 21 | -{{if .Data.Repos}}<h4 class="pinhead">{{if .Data.Pinned}}Pinned{{else}}Recently updated{{end}}<a href="/~{{.Data.Owner.Name}}/repositories">all repositories ▸</a></h4><div class="events">{{range .Data.Repos}}{{template "repoitem" .}}{{end}}</div>{{else}}<p class="info">This organisation does not have any repositories.</p>{{end}} | |
| 21 | +{{if .Data.Repos}}<div class="events">{{range .Data.Repos}}{{template "repoitem" .}}{{end}}</div>{{else}}<p class="info">This organisation does not have any repositories.</p>{{end}} | |
| 22 | 22 | </div> |
| 23 | 23 | <aside class="people"> |
| 24 | 24 | <h4>People <span class="count">{{len .Data.Members}}</span></h4> |
templates/reposettings.html modified
| @@ -5,6 +5,7 @@ | ||
| 5 | 5 | <div> |
| 6 | 6 | <label>Description <input name="description" value="{{$p.Repo.Description}}" maxlength="200"></label> |
| 7 | 7 | <label>Tags <input name="tags" value="{{range $i, $t := $p.Repo.Tags}}{{if $i}}, {{end}}{{$t}}{{end}}" placeholder="Up to three comma-separated topics"></label> |
| 8 | +<label class="check"><input type="checkbox" name="pinned"{{if .Data.Pinned}} checked{{end}}> Pin to profile</label> | |
| 8 | 9 | <div class="actions"><button class="btn primary">Save ▸</button></div> |
| 9 | 10 | </div> |
| 10 | 11 | <div> |
templates/repositories.html modified
| @@ -4,6 +4,5 @@ | ||
| 4 | 4 | <h2>~{{$d.Owner.Name}}</h2> |
| 5 | 5 | <nav class="tabs small"><a href="/~{{$d.Owner.Name}}">about</a><a class="active" href="/~{{$d.Owner.Name}}/repositories">repositories{{if $d.Repos}} <span class="count">{{len $d.Repos}}</span>{{end}}</a>{{if not $d.Org}}<a href="/~{{$d.Owner.Name}}/stars">stars{{if $d.Count}} <span class="count">{{$d.Count}}</span>{{end}}</a>{{end}}<a href="/~{{$d.Owner.Name}}/feed">feed</a></nav> |
| 6 | 6 | </div> |
| 7 | -{{if $d.Member}}<p class="muted small">Pinned repositories show on the profile page, up to {{$d.Max}} of them in the order they were pinned.</p>{{end}} | |
| 8 | -{{if $d.Repos}}<div class="events">{{range $d.Repos}}{{$pin := index $d.Pins .ID}}<div class="event"><h4><a href="/{{.Path}}">{{.Name}}</a>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if $pin}} <span class="tag">pinned</span>{{end}}{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}{{if $d.Member}}<form method="post" action="/~{{$d.Owner.Name}}/repositories" class="pinform"><input type="hidden" name="repo" value="{{.ID}}"><button class="btn small" name="action" value="{{if $pin}}unpin{{else}}pin{{end}}">{{if $pin}}Unpin{{else}}Pin{{end}}</button></form>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="info">No repositories yet.</p>{{end}} | |
| 7 | +{{if $d.Repos}}<div class="events">{{range $d.Repos}}{{$pin := index $d.Pins .ID}}<div class="event"><h4><a href="/{{.Path}}">{{.Name}}</a>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if $pin}} <span class="tag">pinned</span>{{end}}{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="info">No repositories yet.</p>{{end}} | |
| 9 | 8 | {{end}} |
templates/user.html modified
| @@ -21,7 +21,7 @@ | ||
| 21 | 21 | </aside> |
| 22 | 22 | <div class="main"> |
| 23 | 23 | {{with .Data.Activity}}<section class="activity"><div class="heathead"><p class="muted small">{{.Total}} commit{{if ne .Total 1}}s{{end}} {{if .Year}}in {{.Year}}{{else}}in the past 12 months{{end}}</p><nav class="years"><a href="?"{{if not .Year}} class="active"{{end}}>past 12 months</a>{{range .Years}}<a href="?year={{.}}"{{if eq . $.Data.Activity.Year}} class="active"{{end}}>{{.}}</a>{{end}}</nav></div>{{.Grid}}<p class="legend muted small">less <i class="h0"></i><i class="h1"></i><i class="h2"></i><i class="h3"></i><i class="h4"></i> more</p></section>{{end}} |
| 24 | -{{if .Data.Repos}}<h4 class="pinhead">{{if .Data.Pinned}}Pinned{{else}}Recently updated{{end}}<a href="/~{{.Data.Owner.Name}}/repositories">all repositories ▸</a></h4><div class="events">{{range .Data.Repos}}{{template "repoitem" .}}{{end}}</div>{{else}}<p class="info">This user does not have any repositories.</p>{{end}} | |
| 24 | +{{if .Data.Repos}}<div class="events">{{range .Data.Repos}}{{template "repoitem" .}}{{end}}</div>{{else}}<p class="info">This user does not have any repositories.</p>{{end}} | |
| 25 | 25 | </div> |
| 26 | 26 | </div> |
| 27 | 27 | {{end}} |
web.go modified
| @@ -868,7 +868,7 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) { | ||
| 868 | 868 | s.orgSettings(w, r, owner, name) |
| 869 | 869 | return |
| 870 | 870 | } |
| 871 | - if name == "repositories" && len(parts) == 2 { | |
| 871 | + if name == "repositories" && len(parts) == 2 && r.Method == "GET" { | |
| 872 | 872 | s.repositories(w, r, owner, user) |
| 873 | 873 | return |
| 874 | 874 | } |
| @@ -1382,7 +1382,7 @@ func (s *Server) repoSettings(w http.ResponseWriter, r *http.Request, repo Repo) | ||
| 1382 | 1382 | rp.Tab = "settings" |
| 1383 | 1383 | collabs, _ := s.store.Collaborators(repo.ID) |
| 1384 | 1384 | labels, _ := s.store.Labels(repo.ID) |
| 1385 | - s.render(w, r, "reposettings", page{Title: repo.Path() + " settings", Error: r.URL.Query().Get("error"), Data: map[string]any{"Page": rp, "Collaborators": collabs, "Labels": labels, "Protections": s.store.Protections(repo.ID)}}) | |
| 1385 | + s.render(w, r, "reposettings", page{Title: repo.Path() + " settings", Error: r.URL.Query().Get("error"), Data: map[string]any{"Page": rp, "Collaborators": collabs, "Labels": labels, "Protections": s.store.Protections(repo.ID), "Pinned": s.store.Pins(repo.Owner.ID)[repo.ID] > 0}}) | |
| 1386 | 1386 | } |
| 1387 | 1387 | |
| 1388 | 1388 | func (s *Server) collaborators(w http.ResponseWriter, r *http.Request, repo Repo) { |
| @@ -1413,7 +1413,11 @@ func (s *Server) updateRepo(w http.ResponseWriter, r *http.Request, repo Repo) { | ||
| 1413 | 1413 | s.fail(w, r, 403, "Not your repository") |
| 1414 | 1414 | return |
| 1415 | 1415 | } |
| 1416 | - if err := s.store.UpdateRepo(repo.ID, strings.TrimSpace(r.FormValue("description")), visibility(r.FormValue("visibility")), r.FormValue("tags")); err != nil { | |
| 1416 | + err := s.store.UpdateRepo(repo.ID, strings.TrimSpace(r.FormValue("description")), visibility(r.FormValue("visibility")), r.FormValue("tags")) | |
| 1417 | + if pin := r.FormValue("pinned") != ""; err == nil && pin != (s.store.Pins(repo.Owner.ID)[repo.ID] > 0) { | |
| 1418 | + err = s.store.Pin(repo.Owner.ID, repo.ID, pin) | |
| 1419 | + } | |
| 1420 | + if err != nil { | |
| 1417 | 1421 | rp.Tab = "settings" |
| 1418 | 1422 | w.WriteHeader(400) |
| 1419 | 1423 | s.render(w, r, "reposettings", page{Title: repo.Path() + " settings", Error: err.Error(), Data: map[string]any{"Page": rp}}) |
| @@ -1601,27 +1605,12 @@ func pinnedRepos(repos []Repo, pins map[int64]int) []Repo { | ||
| 1601 | 1605 | |
| 1602 | 1606 | func (s *Server) repositories(w http.ResponseWriter, r *http.Request, owner User, user *User) { |
| 1603 | 1607 | member := user != nil && (user.ID == owner.ID || s.store.IsMember(owner.ID, user.ID)) |
| 1604 | - if r.Method == "POST" { | |
| 1605 | - if !member { | |
| 1606 | - s.fail(w, r, 404, "Not found") | |
| 1607 | - return | |
| 1608 | - } | |
| 1609 | - id, _ := strconv.ParseInt(r.FormValue("repo"), 10, 64) | |
| 1610 | - if err := s.store.Pin(owner.ID, id, r.FormValue("action") == "pin"); err != nil { | |
| 1611 | - http.Redirect(w, r, "/~"+owner.Name+"/repositories?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) | |
| 1612 | - return | |
| 1613 | - } | |
| 1614 | - http.Redirect(w, r, "/~"+owner.Name+"/repositories", http.StatusSeeOther) | |
| 1615 | - return | |
| 1616 | - } | |
| 1617 | 1608 | repos, err := s.store.ReposByOwner(owner.ID, member) |
| 1618 | 1609 | if err != nil { |
| 1619 | 1610 | s.fail(w, r, 500, err.Error()) |
| 1620 | 1611 | return |
| 1621 | 1612 | } |
| 1622 | - repos = s.byUpdate(repos) | |
| 1623 | 1613 | starred, _ := s.store.StarredRepos(owner.ID) |
| 1624 | - pins := s.store.Pins(owner.ID) | |
| 1625 | - data := map[string]any{"Owner": owner, "Repos": repos, "Pins": pins, "Member": member, "Count": len(starred), "Org": s.store.IsOrg(owner.ID), "Max": maxPins} | |
| 1626 | - s.render(w, r, "repositories", page{Title: "~" + owner.Name + " repositories", User: user, Error: r.URL.Query().Get("error"), Data: data}) | |
| 1614 | + data := map[string]any{"Owner": owner, "Repos": s.byUpdate(repos), "Pins": s.store.Pins(owner.ID), "Count": len(starred), "Org": s.store.IsOrg(owner.ID)} | |
| 1615 | + s.render(w, r, "repositories", page{Title: "~" + owner.Name + " repositories", User: user, Data: data}) | |
| 1627 | 1616 | } |