Monorepo for Tangled tangled.org

appview: allows a default knot to be configured #991

open opened by willdot.net targeting master from willdot.net/tangled-fork: default-knot

This follows on from the work carried out in #836

I've added a select box in the Knots settings page which pulls in the users knots and also adds in knot1.tangled.sh. When the user selects one of these options, it will save to their profile in the database. NOTE: I haven't yet implemented adding that to the AT record because I'm not sure on how the lexicon setup works yet!

Then when users go to create a new repo / fork, if there is a value in their profile for the default knot, then that will pre select the knot to use for the new repo / fork.

This is a duplicate of https://tangled.org/tangled.org/core/pulls/858/ which had to be closed because there were merge conflicts which couldn't be resolved due to the origin fork being deleted ๐Ÿ™ˆ

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:dadhhalkfcq3gucaq25hjqon/sh.tangled.repo.pull/3mcrzfonnxs22
+78 -53
Interdiff #4 โ†’ #5
+6 -7
appview/db/db.go
··· 567 567 unique (from_at, to_at) 568 568 ); 569 569 570 + create table if not exists knot_preferences ( 571 + id integer primary key autoincrement, 572 + user_did text not null unique, 573 + default_knot text 574 + ); 575 + 570 576 create table if not exists migrations ( 571 577 id integer primary key autoincrement, 572 578 name text unique ··· 1173 1179 return err 1174 1180 }) 1175 1181 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 - `) 1180 - return err 1181 - }) 1182 - 1183 1182 return &DB{ 1184 1183 db, 1185 1184 logger,
+3 -10
appview/db/profile.go
··· 141 141 pronouns, 142 142 default_knot 143 143 ) 144 - values (?, ?, ?, ?, ?, ?)`, 144 + values (?, ?, ?, ?, ?)`, 145 145 profile.Did, 146 146 profile.Description, 147 147 includeBskyValue, 148 148 profile.Location, 149 149 profile.Pronouns, 150 - profile.DefaultKnot, 151 150 ) 152 151 153 152 if err != nil { ··· 326 325 func GetProfile(e Execer, did string) (*models.Profile, error) { 327 326 var profile models.Profile 328 327 var pronouns sql.Null[string] 329 - var defaultKnot sql.Null[string] 330 - 331 328 profile.Did = did 332 329 333 330 includeBluesky := 0 334 331 335 332 err := e.QueryRow( 336 - `select description, include_bluesky, location, pronouns, default_knot from profile where did = ?`, 333 + `select description, include_bluesky, location, pronouns from profile where did = ?`, 337 334 did, 338 - ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns, &defaultKnot) 335 + ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns) 339 336 if err == sql.ErrNoRows { 340 337 profile := models.Profile{} 341 338 profile.Did = did ··· 354 351 profile.Pronouns = pronouns.V 355 352 } 356 353 357 - if defaultKnot.Valid { 358 - profile.DefaultKnot = defaultKnot.V 359 - } 360 - 361 354 rows, err := e.Query(`select link from profile_links where did = ?`, did) 362 355 if err != nil { 363 356 return nil, err
+4 -4
appview/knots/knots.go
··· 89 89 } 90 90 91 91 defaultKnot := "" 92 - profile, err := db.GetProfile(k.Db, user.Did()) 92 + knotPrefence, err := db.GetKnotPreference(k.Db, user.Did()) 93 93 if err != nil { 94 - k.Logger.Warn("gettings user profile to get default knot", "error", err) 94 + k.Logger.Warn("gettings users knot preferences", "error", err) 95 95 } 96 - if profile != nil { 97 - defaultKnot = profile.DefaultKnot 96 + if knotPrefence != nil { 97 + defaultKnot = knotPrefence.DefaultKnot 98 98 } 99 99 100 100 k.Pages.Knots(w, pages.KnotsParams{
-1
appview/models/profile.go
··· 20 20 Stats [2]VanityStat 21 21 PinnedRepos [6]syntax.ATURI 22 22 Pronouns string 23 - DefaultKnot string 24 23 } 25 24 26 25 func (p Profile) IsLinksEmpty() bool {
appview/pages/pages.go

This file has not been changed.

appview/pages/templates/knots/index.html

This file has not been changed.

appview/pages/templates/repo/fork.html

This file has not been changed.

appview/pages/templates/repo/new.html

This file has not been changed.

+4 -4
appview/repo/repo.go
··· 1005 1005 } 1006 1006 1007 1007 defaultKnot := "" 1008 - profile, err := db.GetProfile(rp.db, user.Did()) 1008 + knotPrefence, err := db.GetKnotPreference(rp.db, user.Did()) 1009 1009 if err != nil { 1010 - rp.logger.Warn("gettings user profile to get default knot", "error", err) 1010 + rp.logger.Warn("gettings users knot preferences", "error", err) 1011 1011 } 1012 - if profile != nil { 1013 - defaultKnot = profile.DefaultKnot 1012 + if knotPrefence != nil { 1013 + defaultKnot = knotPrefence.DefaultKnot 1014 1014 } 1015 1015 1016 1016 rp.pages.ForkRepo(w, pages.ForkRepoParams{
+5 -22
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) { 619 + func (s *State) UpdateDefaultKnotPreference(w http.ResponseWriter, r *http.Request) { 620 620 err := r.ParseForm() 621 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 { 622 + log.Println("invalid preference update form", err) 633 623 return 634 624 } 625 + user := s.oauth.GetMultiAccountUser(r) 635 626 636 627 defaultKnot := r.Form.Get("default-knot") 637 628 ··· 639 630 defaultKnot = "" 640 631 } 641 632 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) 633 + err = db.UpsertKnotPreference(s.db, user.Did(), defaultKnot) 651 634 if err != nil { 652 - log.Println("failed to update profile", err) 635 + log.Println("failed to update default knot preference", err) 653 636 return 654 637 } 655 638
+1 -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 + r.Post("/default-knot", s.UpdateDefaultKnotPreference) 169 169 }) 170 170 171 171 r.Mount("/settings", s.SettingsRouter())
+6 -4
appview/state/state.go
··· 418 418 return 419 419 } 420 420 421 + knots = append(knots, "hello") 422 + 421 423 defaultKnot := "" 422 - profile, err := db.GetProfile(s.db, user.Did()) 424 + knotPrefence, err := db.GetKnotPreference(s.db, user.Did()) 423 425 if err != nil { 424 - s.logger.Warn("gettings user profile to get default knot", "error", err) 426 + s.logger.Warn("gettings users knot preferences", "error", err) 425 427 } 426 - if profile != nil { 427 - defaultKnot = profile.DefaultKnot 428 + if knotPrefence != nil { 429 + defaultKnot = knotPrefence.DefaultKnot 428 430 } 429 431 430 432 s.pages.NewRepo(w, pages.NewRepoParams{
+42
appview/db/preferences.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + 6 + "tangled.org/core/appview/models" 7 + ) 8 + 9 + func GetKnotPreference(e Execer, did string) (*models.KnotPreference, error) { 10 + var knotPreference models.KnotPreference 11 + 12 + err := e.QueryRow( 13 + `select id, user_did, default_knot from knot_preferences where user_did = ?`, 14 + did, 15 + ).Scan(&knotPreference.ID, &knotPreference.Did, &knotPreference.DefaultKnot) 16 + if err == sql.ErrNoRows { 17 + return nil, nil 18 + } 19 + 20 + if err != nil { 21 + return nil, err 22 + } 23 + 24 + return &knotPreference, nil 25 + } 26 + 27 + func UpsertKnotPreference(e Execer, did, defaultKnot string) error { 28 + _, err := e.Exec( 29 + `insert or replace into knot_preferences ( 30 + user_did, 31 + default_knot 32 + ) 33 + values (?, ?)`, 34 + did, 35 + defaultKnot, 36 + ) 37 + if err != nil { 38 + return err 39 + } 40 + 41 + return nil 42 + }
+7
appview/models/preferences.go
··· 1 + package models 2 + 3 + type KnotPreference struct { 4 + ID int 5 + Did string 6 + DefaultKnot string 7 + }

History

8 rounds 9 comments
sign up or login to add to the discussion
1 commit
expand
appview: allow default knot to be saved as a preference and then used when creating a new repo
no conflicts, ready to merge
expand 0 comments
1 commit
expand
appview: allow default knot to be saved as a preference and then used when creating a new repo
expand 0 comments
1 commit
expand
appview: allow default knot to be saved as a preference and then used when creating a new repo
expand 0 comments
1 commit
expand
appview: allows a default knot to be selected from knots available to the user
expand 5 comments

@oppi.li While attempting to implement something else related to profiles, I've noticed that UpsertProfile(...) herehttps://tangled.org/tangled.org/core/blob/master/appview/db/profile.go#L133 also gets called as part of a JS ingester here https://tangled.org/tangled.org/core/blob/master/appview/ingester.go#L352 which means the DefaultKnot field will be overwritten with an empty string when a profile event is ingested. Am I understanding that correctly?

So instead I should probably not use the UpsertProfile(...) but use a smaller scoped function that just sets the Default Knot field?

ah that is correct, the default knot will be overwritten by that call. i suppose that is one of the drawbacks of overloading models.Profile! instead of cloning UpsertProfile to add this as an exception, perhaps this is an indication that we need to avoid using the profile table/model for this. what do you think about creating a preferences table for stuff like this? we already have notification_preferences, we could do something similar for knots.

i see you have a PR open for punchcard work as well, which would fit quite well in a punchcard_preferences table.

Yh I'm 100% down for using that approach. While I was implementing the punchcard PR I had this nagging feeling in me that this and other things belonged in some sort of separate "preferences" table so splitting them out into things like knot_preferences or punchcard_preferences is the right answer for me.

I'll redo that for this PR and then do the same of the other PR and hopefully it will prevent the merge conflicts I was dreading ๐Ÿ™ˆ

yeah! knot_preferences and punchcard_preferences seems okay for now. thanks for keeping at this by the way, and sorry for the slow review cycle!

No worries! I don't expect immediate reviews ๐Ÿ˜

I've refactored to use a new knot_preferences table for this PR. I had some merge conflicts with master though, so rebased that although took me a couple attempts to get everything nice and neat, hence so many rounds ๐Ÿ™ˆ

2 commits
expand
appview: allows a default knot to be configured
appview: allows a default knot to be selected from knots available to the user
expand 0 comments
1 commit
expand
appview: allows a default knot to be configured
expand 2 comments

the code itself works pretty nicely now. so i've noticed a couple issues (apologies for not looking more closely earlier!):

  • the default-knot selector only iterates over knots that the user is an admin of, whereas they should be able to choose any knot that they are a member of as the default. you can use s.enforcer.GetKnotsForUser(...) to get a list of knots that a user has access to
  • the HTML change adds knot1.tangled.sh as an explicit case, this is not necessary if the above is implemented
  • the HTML template currently triggers a request when the select changes, could we make this more like how the spindle selector works? a form with a button to submit.

I was looking for ages trying to find somewhere that did something similar. I don't use spindles so never saw that ๐Ÿ™ˆ A much better approach.

Also good shout on the knots for a user. Reading the code now, I understand what the registrations are now.

Anyhow, I've updated the PR and squashed everything so it should be good to test now.

Please note, that I have yet to figure out how to get multiple Knots running locally, and since I don't get knot1.tangled.sh locally I had to hard code some things in to test it worked, which is less than ideal.

1 commit
expand
appview: allows a default knot to be configured
expand 0 comments
1 commit
expand
appview: allows a default knot to be configured
expand 2 comments

i get this error at runtime:

2026/01/20 04:58:12 profile err 5 values for 6 columns
2026/01/20 04:58:12 failed to update profile 5 values for 6 columns

this sql query is incorrect:

		`insert or replace into profile (
			did,
			description,
			include_bluesky,
			location,
			pronouns,
			default_knot
		)
		values (?, ?, ?, ?, ?)`,

there should be 6 ? placeholders!

Ah crap, someone needs to create a lint tool that detects that ๐Ÿคฆโ€โ™‚๏ธ

Fixed.