Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview/pages: add spindle selector in repo/fork pages

both pages now have an "advanced" section for users to configure
spindles, the default selection is "No Spindle"

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
committer
Tangled
date (Jul 22, 2026, 1:19 PM +0300) commit 25f1f56b parent 05294586 change-id uqxllrkz
+171 -4
+3 -1
appview/pages/pages.go
··· 704 704 705 705 type NewRepoParams struct { 706 706 BaseParams 707 - Knots []string 707 + Knots []string 708 + Spindles []string 708 709 } 709 710 710 711 func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { ··· 714 715 type ForkRepoParams struct { 715 716 BaseParams 716 717 Knots []string 718 + Spindles []string 717 719 RepoInfo repoinfo.RepoInfo 718 720 } 719 721
+57 -1
appview/pages/templates/repo/fork.html
··· 124 124 </div> 125 125 </div> 126 126 127 - <div class="py-8 flex justify-end border-l border-gray-200 dark:border-gray-700 pl-6"> 127 + {{/* Step 3 — Advanced */}} 128 + <details class="group/advanced"> 129 + <summary class="flex gap-4 items-center relative border-l border-gray-200 dark:border-gray-700 pl-6 cursor-pointer list-none"> 130 + <div class="absolute top-1/2 -translate-y-1/2 -left-[11px]"> 131 + <div class="size-6 bg-gray-200 dark:bg-gray-600 dark:text-white rounded-full flex items-center justify-center"> 132 + {{ i "plus" "size-4" }} 133 + </div> 134 + </div> 135 + <div class="flex-1 flex flex-col gap-1"> 136 + <h2 class="text-lg font-medium dark:text-white">Advanced</h2> 137 + </div> 138 + </summary> 139 + 140 + <div class="flex gap-4 relative border-l border-gray-200 dark:border-gray-700 pl-6 pb-6"> 141 + <div class="flex-1 flex flex-col gap-2 pt-4"> 142 + <div class="space-y-4"> 143 + 144 + {{/* Spindle list */}} 145 + <div> 146 + <label class="block text-sm font-bold dark:text-white mb-1">Select a spindle</label> 147 + <div class="w-full dark:bg-gray-800 dark:text-white dark:border-gray-700 space-y-2"> 148 + <div class="flex items-center"> 149 + <input 150 + type="radio" 151 + name="spindle" 152 + value="" 153 + id="spindle-none" 154 + checked 155 + class="mr-2" 156 + /> 157 + <label for="spindle-none" class="dark:text-white">No spindle</label> 158 + </div> 159 + {{ range .Spindles }} 160 + <div class="flex items-center"> 161 + <input 162 + type="radio" 163 + name="spindle" 164 + value="{{ . }}" 165 + id="spindle-{{ . }}" 166 + class="mr-2" 167 + /> 168 + <label for="spindle-{{ . }}" class="dark:text-white lowercase">{{ . }}</label> 169 + </div> 170 + {{ end }} 171 + </div> 172 + <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"> 173 + A spindle runs your CI workflows. 174 + You can also <a href="/settings/spindles" class="underline">register your own spindle</a>. 175 + </p> 176 + </div> 177 + 178 + </div> 179 + </div> 180 + </div> 181 + </details> 182 + 183 + <div class="py-8 flex justify-end"> 128 184 <button type="submit" class="btn-create flex items-center gap-2"> 129 185 {{ i "git-fork" "size-4" }} 130 186 Fork repository
+65 -2
appview/pages/templates/repo/new.html
··· 23 23 <form hx-post="/repo/new" hx-swap="none" hx-indicator="#spinner"> 24 24 {{ template "step-1" . }} 25 25 {{ template "step-2" . }} 26 + {{ template "step-3" . }} 26 27 27 28 <div class="mt-8 flex justify-end"> 28 29 <button type="submit" class="btn-create flex items-center gap-2"> ··· 63 64 {{ template "numberCircle" 2 }} 64 65 </div> 65 66 66 - <div class="flex-1"> 67 + <div class="flex-1 pb-12"> 67 68 <h2 class="text-lg font-medium dark:text-white">Configuration</h2> 68 69 <div class="text-sm text-gray-500 dark:text-gray-400 mb-4">Repository settings and hosting.</div> 69 70 ··· 73 74 </div> 74 75 </div> 75 76 </div> 77 + {{ end }} 78 + 79 + {{ define "step-3" }} 80 + <details class="group/advanced"> 81 + <summary class="flex gap-4 items-center relative border-l border-gray-200 dark:border-gray-700 pl-6 cursor-pointer list-none"> 82 + <div class="absolute -left-3 top-1/2 -translate-y-1/2"> 83 + <div class="w-6 h-6 bg-gray-200 dark:bg-gray-600 dark:text-white rounded-full flex items-center justify-center"> 84 + {{ i "plus" "w-4 h-4" }} 85 + </div> 86 + </div> 87 + 88 + <div class="flex-1"> 89 + <h2 class="text-lg font-medium dark:text-white">Advanced</h2> 90 + </div> 91 + </summary> 92 + 93 + <div class="flex gap-4 relative border-l border-gray-200 dark:border-gray-700 pl-6"> 94 + <div class="flex-1 pt-4"> 95 + <div class="space-y-2"> 96 + {{ template "spindle" . }} 97 + </div> 98 + </div> 99 + </div> 100 + </details> 76 101 {{ end }} 77 102 78 103 {{ define "name" }} ··· 171 196 </div> 172 197 {{ end }} 173 198 199 + {{ define "spindle" }} 200 + <!-- Spindle Selection --> 201 + <div> 202 + <label class="block text-sm font-bold dark:text-white mb-1"> 203 + Select a spindle 204 + </label> 205 + <div class="w-full space-y-2"> 206 + <div class="flex items-center"> 207 + <input 208 + type="radio" 209 + name="spindle" 210 + value="" 211 + class="mr-2" 212 + id="spindle-none" 213 + checked 214 + /> 215 + <label for="spindle-none" class="dark:text-white">No spindle</label> 216 + </div> 217 + {{ range .Spindles }} 218 + <div class="flex items-center"> 219 + <input 220 + type="radio" 221 + name="spindle" 222 + value="{{ . }}" 223 + class="mr-2" 224 + id="spindle-{{ . }}" 225 + /> 226 + <label for="spindle-{{ . }}" class="dark:text-white lowercase">{{ . }}</label> 227 + </div> 228 + {{ end }} 229 + </div> 230 + <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"> 231 + A spindle runs your CI workflows. 232 + You can also <a href="/settings/spindles" class="underline">register your own spindle</a>. 233 + </p> 234 + </div> 235 + {{ end }} 236 + 174 237 {{ define "numberCircle" }} 175 - <div class="w-6 h-6 bg-gray-200 dark:bg-gray-600 rounded-full flex items-center justify-center text-sm font-medium mt-1"> 238 + <div class="w-6 h-6 bg-gray-200 dark:bg-gray-600 rounded-full flex items-center justify-center text-sm font-medium"> 176 239 {{.}} 177 240 </div> 178 241 {{ end }}
+22
appview/repo/repo.go
··· 1418 1418 user := rp.oauth.GetMultiAccountUser(r) 1419 1419 knots := rp.acl.KnotsForUser(r.Context(), user.Did) 1420 1420 1421 + spindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 1422 + if err != nil { 1423 + l.Error("failed to fetch spindles", "err", err) 1424 + } 1425 + 1421 1426 rp.pages.ForkRepo(w, pages.ForkRepoParams{ 1422 1427 BaseParams: pages.BaseParamsFromContext(r.Context()), 1423 1428 Knots: knots, 1429 + Spindles: spindles, 1424 1430 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 1425 1431 }) 1426 1432 ··· 1437 1443 if !rp.acl.IsRepoCreateAllowed(r.Context(), targetKnot, user.Did) { 1438 1444 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1439 1445 return 1446 + } 1447 + 1448 + // optional spindle selection; validate the user is a member if provided 1449 + spindle := r.FormValue("spindle") 1450 + if spindle != "" { 1451 + validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 1452 + if err != nil { 1453 + l.Error("failed to fetch spindles", "err", err) 1454 + rp.pages.Notice(w, "repo", "Failed to configure spindle. Try again later.") 1455 + return 1456 + } 1457 + if !slices.Contains(validSpindles, spindle) { 1458 + rp.pages.Notice(w, "repo", "Invalid spindle selection.") 1459 + return 1460 + } 1440 1461 } 1441 1462 1442 1463 // choose a name for a fork ··· 1530 1551 Rkey: rkey, 1531 1552 Source: forkSource, 1532 1553 Description: forkDescription, 1554 + Spindle: spindle, 1533 1555 Created: time.Now(), 1534 1556 Labels: rp.config.Label.DefaultLabelDefs, 1535 1557 RepoDid: repoDid,
+24
appview/state/state.go
··· 7 7 "fmt" 8 8 "log/slog" 9 9 "net/http" 10 + "slices" 10 11 "strings" 11 12 "time" 12 13 ··· 449 450 user := s.oauth.GetMultiAccountUser(r) 450 451 knots := s.aclService.KnotsForUser(r.Context(), user.Did) 451 452 453 + spindles, err := s.enforcer.GetSpindlesForUser(user.Did) 454 + if err != nil { 455 + s.logger.Error("failed to fetch spindles", "err", err) 456 + } 457 + 452 458 s.pages.NewRepo(w, pages.NewRepoParams{ 453 459 BaseParams: pages.BaseParamsFromContext(r.Context()), 454 460 Knots: knots, 461 + Spindles: spindles, 455 462 }) 456 463 457 464 case http.MethodPost: ··· 493 500 s.pages.Notice(w, "repo", "Description must be 140 characters or fewer.") 494 501 return 495 502 } 503 + 504 + // optional spindle selection; validate the user is a member if provided 505 + spindle := r.FormValue("spindle") 506 + if spindle != "" { 507 + validSpindles, err := s.enforcer.GetSpindlesForUser(user.Did) 508 + if err != nil { 509 + l.Error("failed to fetch spindles", "err", err) 510 + s.pages.Notice(w, "repo", "Failed to configure spindle. Try again later.") 511 + return 512 + } 513 + if !slices.Contains(validSpindles, spindle) { 514 + s.pages.Notice(w, "repo", "Invalid spindle selection.") 515 + return 516 + } 517 + } 518 + l = l.With("spindle", spindle) 496 519 497 520 // ACL validation 498 521 if !s.aclService.IsRepoCreateAllowed(r.Context(), domain, user.Did) { ··· 570 593 Knot: domain, 571 594 Rkey: rkey, 572 595 Description: description, 596 + Spindle: spindle, 573 597 Created: time.Now(), 574 598 Labels: s.config.Label.DefaultLabelDefs, 575 599 RepoDid: repoDid,