Monorepo for Tangled

appview: allows a default knot to be selected from knots available to the user

Signed-off-by: Will Andrews <will7989@hotmail.com>

+145 -15
+8 -1
appview/db/db.go
··· 1078 1078 // transfer data, constructing pull_at from pulls table 1079 1079 _, err = tx.Exec(` 1080 1080 insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1081 - select 1081 + select 1082 1082 ps.id, 1083 1083 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1084 1084 ps.round_number, ··· 1170 1170 create index if not exists idx_stars_created on stars(created); 1171 1171 create index if not exists idx_stars_subject_at_created on stars(subject_at, created); 1172 1172 `) 1173 + return err 1174 + }) 1175 + 1176 + orm.RunMigration(conn, logger, "add-default-knot-profile", func(tx *sql.Tx) error { 1177 + _, err := tx.Exec(` 1178 + alter table profile add column default_knot text; 1179 + `) 1173 1180 return err 1174 1181 }) 1175 1182
+11 -4
appview/db/profile.go
··· 138 138 description, 139 139 include_bluesky, 140 140 location, 141 - pronouns 141 + pronouns, 142 + default_knot 142 143 ) 143 - values (?, ?, ?, ?, ?)`, 144 + values (?, ?, ?, ?, ?, ?)`, 144 145 profile.Did, 145 146 profile.Description, 146 147 includeBskyValue, 147 148 profile.Location, 148 149 profile.Pronouns, 150 + profile.DefaultKnot, 149 151 ) 150 152 151 153 if err != nil { ··· 324 326 func GetProfile(e Execer, did string) (*models.Profile, error) { 325 327 var profile models.Profile 326 328 var pronouns sql.Null[string] 329 + var defaultKnot sql.Null[string] 327 330 328 331 profile.Did = did 329 332 330 333 includeBluesky := 0 331 334 332 335 err := e.QueryRow( 333 - `select description, include_bluesky, location, pronouns from profile where did = ?`, 336 + `select description, include_bluesky, location, pronouns, default_knot from profile where did = ?`, 334 337 did, 335 - ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns) 338 + ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns, &defaultKnot) 336 339 if err == sql.ErrNoRows { 337 340 profile := models.Profile{} 338 341 profile.Did = did ··· 349 352 350 353 if pronouns.Valid { 351 354 profile.Pronouns = pronouns.V 355 + } 356 + 357 + if defaultKnot.Valid { 358 + profile.DefaultKnot = defaultKnot.V 352 359 } 353 360 354 361 rows, err := e.Query(`select link from profile_links where did = ?`, did)
+22 -4
appview/knots/knots.go
··· 81 81 return 82 82 } 83 83 84 + availableKnots, err := k.Enforcer.GetKnotsForUser(user.Did()) 85 + if err != nil { 86 + k.Logger.Error("failed to fetch available knots for user", "err", err) 87 + w.WriteHeader(http.StatusInternalServerError) 88 + return 89 + } 90 + 91 + defaultKnot := "" 92 + profile, err := db.GetProfile(k.Db, user.Did()) 93 + if err != nil { 94 + k.Logger.Warn("gettings user profile to get default knot", "error", err) 95 + } 96 + if profile != nil { 97 + defaultKnot = profile.DefaultKnot 98 + } 99 + 84 100 k.Pages.Knots(w, pages.KnotsParams{ 85 - LoggedInUser: user, 86 - Registrations: registrations, 87 - Tabs: knotsTabs, 88 - Tab: "knots", 101 + LoggedInUser: user, 102 + Registrations: registrations, 103 + Tabs: knotsTabs, 104 + Tab: "knots", 105 + AvailableKnots: availableKnots, 106 + DefaultKnot: defaultKnot, 89 107 }) 90 108 } 91 109
+1
appview/models/profile.go
··· 20 20 Stats [2]VanityStat 21 21 PinnedRepos [6]syntax.ATURI 22 22 Pronouns string 23 + DefaultKnot string 23 24 } 24 25 25 26 func (p Profile) IsLinksEmpty() bool {
+8 -4
appview/pages/pages.go
··· 417 417 } 418 418 419 419 type KnotsParams struct { 420 - LoggedInUser *oauth.MultiAccountUser 421 - Registrations []models.Registration 422 - Tabs []map[string]any 423 - Tab string 420 + LoggedInUser *oauth.MultiAccountUser 421 + Registrations []models.Registration 422 + Tabs []map[string]any 423 + Tab string 424 + AvailableKnots []string 425 + DefaultKnot string 424 426 } 425 427 426 428 func (p *Pages) Knots(w io.Writer, params KnotsParams) error { ··· 486 488 type NewRepoParams struct { 487 489 LoggedInUser *oauth.MultiAccountUser 488 490 Knots []string 491 + DefaultKnot string 489 492 } 490 493 491 494 func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { ··· 496 499 LoggedInUser *oauth.MultiAccountUser 497 500 Knots []string 498 501 RepoInfo repoinfo.RepoInfo 502 + DefaultKnot string 499 503 } 500 504 501 505 func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
+28
appview/pages/templates/knots/index.html
··· 31 31 <div class="flex flex-col gap-6"> 32 32 {{ block "list" . }} {{ end }} 33 33 {{ block "register" . }} {{ end }} 34 + {{ block "default-knot" . }} {{ end }} 34 35 </div> 35 36 </section> 36 37 {{ end }} ··· 60 61 </div> 61 62 <div id="operation-error" class="text-red-500 dark:text-red-400"></div> 62 63 </section> 64 + {{ end }} 65 + 66 + {{ define "default-knot" }} 67 + <section class="rounded w-full flex flex-col gap-2"> 68 + <h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">default knot</h2> 69 + <form hx-post="/profile/default-knot" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch"> 70 + <select 71 + id="default-knot" 72 + name="default-knot" 73 + required 74 + class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"> 75 + {{/* For some reason, we can't use an empty string in a <select> in all scenarios unless it is preceded by a disabled select?? No idea, could just be a Firefox thing? */}} 76 + <option value="[[none]]" class="py-1" {{ if not $.DefaultKnot }}selected{{ end }}> 77 + Choose a default knot 78 + </option> 79 + {{ range $.AvailableKnots }} 80 + <option value="{{ . }}" class="py-1" {{ if eq . $.DefaultKnot }}selected{{ end }}> 81 + {{ . }} 82 + </option> 83 + {{ end }} 84 + </select> 85 + <button class="btn flex gap-2 items-center" type="submit"> 86 + {{ i "check" "size-4" }} 87 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 88 + </button> 89 + </form> 90 + </section> 63 91 {{ end }} 64 92 65 93 {{ define "register" }}
+3 -1
appview/pages/templates/repo/fork.html
··· 25 25 value="{{ . }}" 26 26 class="mr-2" 27 27 id="domain-{{ . }}" 28 - {{if eq (len $.Knots) 1}}checked{{end}} 28 + {{if eq (len $.Knots) 1}}checked 29 + {{else if eq $.DefaultKnot . }}checked 30 + {{end}} 29 31 /> 30 32 <label for="domain-{{ . }}" class="dark:text-white">{{ . }}</label> 31 33 </div>
+3 -1
appview/pages/templates/repo/new.html
··· 155 155 class="mr-2" 156 156 id="domain-{{ . }}" 157 157 required 158 - {{if eq (len $.Knots) 1}}checked{{end}} 158 + {{if eq (len $.Knots) 1}}checked 159 + {{else if eq $.DefaultKnot . }}checked 160 + {{end}} 159 161 /> 160 162 <label for="domain-{{ . }}" class="dark:text-white lowercase">{{ . }}</label> 161 163 </div>
+10
appview/repo/repo.go
··· 1004 1004 return 1005 1005 } 1006 1006 1007 + defaultKnot := "" 1008 + profile, err := db.GetProfile(rp.db, user.Did()) 1009 + if err != nil { 1010 + rp.logger.Warn("gettings user profile to get default knot", "error", err) 1011 + } 1012 + if profile != nil { 1013 + defaultKnot = profile.DefaultKnot 1014 + } 1015 + 1007 1016 rp.pages.ForkRepo(w, pages.ForkRepoParams{ 1008 1017 LoggedInUser: user, 1009 1018 Knots: knots, 1010 1019 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 1020 + DefaultKnot: defaultKnot, 1011 1021 }) 1012 1022 1013 1023 case http.MethodPost:
+40
appview/state/profile.go
··· 616 616 s.updateProfile(profile, w, r) 617 617 } 618 618 619 + func (s *State) UpdateProfileDefaultKnot(w http.ResponseWriter, r *http.Request) { 620 + err := r.ParseForm() 621 + if err != nil { 622 + log.Println("invalid profile update form", err) 623 + return 624 + } 625 + user := s.oauth.GetUser(r) 626 + 627 + profile, err := db.GetProfile(s.db, user.Did) 628 + if err != nil { 629 + log.Printf("getting profile data for %s: %s", user.Did, err) 630 + } 631 + 632 + if profile == nil { 633 + return 634 + } 635 + 636 + defaultKnot := r.Form.Get("default-knot") 637 + 638 + if defaultKnot == "[[none]]" { // see pages/templates/knots/index.html for more info on why we use this value 639 + defaultKnot = "" 640 + } 641 + 642 + profile.DefaultKnot = defaultKnot 643 + 644 + tx, err := s.db.BeginTx(r.Context(), nil) 645 + if err != nil { 646 + log.Println("failed to start transaction", err) 647 + return 648 + } 649 + 650 + err = db.UpsertProfile(tx, profile) 651 + if err != nil { 652 + log.Println("failed to update profile", err) 653 + return 654 + } 655 + 656 + s.pages.HxRefresh(w) 657 + } 658 + 619 659 func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) { 620 660 user := s.oauth.GetMultiAccountUser(r) 621 661 tx, err := s.db.BeginTx(r.Context(), nil)
+1
appview/state/router.go
··· 165 165 r.Get("/edit-pins", s.EditPinsFragment) 166 166 r.Post("/bio", s.UpdateProfileBio) 167 167 r.Post("/pins", s.UpdateProfilePins) 168 + r.Post("/default-knot", s.UpdateProfileDefaultKnot) 168 169 }) 169 170 170 171 r.Mount("/settings", s.SettingsRouter())
+10
appview/state/state.go
··· 418 418 return 419 419 } 420 420 421 + defaultKnot := "" 422 + profile, err := db.GetProfile(s.db, user.Did()) 423 + if err != nil { 424 + s.logger.Warn("gettings user profile to get default knot", "error", err) 425 + } 426 + if profile != nil { 427 + defaultKnot = profile.DefaultKnot 428 + } 429 + 421 430 s.pages.NewRepo(w, pages.NewRepoParams{ 422 431 LoggedInUser: user, 423 432 Knots: knots, 433 + DefaultKnot: defaultKnot, 424 434 }) 425 435 426 436 case http.MethodPost: