This repository has no description
0

Configure Feed

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

core / appview / pages / pages.go
46 kB 1740 lines
1package pages 2 3import ( 4 "crypto/sha256" 5 "embed" 6 "encoding/hex" 7 "fmt" 8 "html/template" 9 "io" 10 "io/fs" 11 "log/slog" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strings" 16 "sync" 17 "time" 18 19 "tangled.org/core/api/tangled" 20 "tangled.org/core/appview/cache" 21 "tangled.org/core/appview/commitverify" 22 "tangled.org/core/appview/config" 23 "tangled.org/core/appview/db" 24 "tangled.org/core/appview/models" 25 "tangled.org/core/appview/oauth" 26 "tangled.org/core/appview/pages/markup" 27 "tangled.org/core/appview/pages/repoinfo" 28 "tangled.org/core/appview/pagination" 29 "tangled.org/core/idresolver" 30 "tangled.org/core/patchutil" 31 "tangled.org/core/types" 32 33 "github.com/bluesky-social/indigo/atproto/identity" 34 "github.com/bluesky-social/indigo/atproto/syntax" 35 "github.com/go-git/go-git/v5/plumbing" 36) 37 38//go:embed templates/* static legal 39var Files embed.FS 40 41type Pages struct { 42 mu sync.RWMutex 43 cache *TmplCache[string, *template.Template] 44 45 avatar config.AvatarConfig 46 pdsCfg config.PdsConfig 47 resolver *idresolver.Resolver 48 db *db.DB 49 rdb *cache.Cache 50 dev bool 51 embedFS fs.FS 52 templateDir string // Path to templates on disk for dev mode 53 rctx *markup.RenderContext 54 logger *slog.Logger 55} 56 57func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages { 58 // initialized with safe defaults, can be overridden per use 59 rctx := &markup.RenderContext{ 60 IsDev: config.Core.Dev, 61 Hostname: config.Core.AppviewHost, 62 CamoUrl: config.Camo.Host, 63 CamoSecret: config.Camo.SharedSecret, 64 Sanitizer: markup.NewSanitizer(), 65 Files: Files, 66 } 67 68 p := &Pages{ 69 mu: sync.RWMutex{}, 70 cache: NewTmplCache[string, *template.Template](), 71 dev: config.Core.Dev, 72 avatar: config.Avatar, 73 pdsCfg: config.Pds, 74 rctx: rctx, 75 resolver: res, 76 db: database, 77 rdb: rdb, 78 templateDir: "appview/pages", 79 logger: logger, 80 } 81 82 if p.dev { 83 p.embedFS = os.DirFS(p.templateDir) 84 } else { 85 p.embedFS = Files 86 } 87 88 return p 89} 90 91// reverse of pathToName 92func (p *Pages) nameToPath(s string) string { 93 return "templates/" + s + ".html" 94} 95 96// FuncMap returns the template function map for use by external template consumers. 97func (p *Pages) FuncMap() template.FuncMap { 98 return p.funcMap() 99} 100 101// FragmentPaths returns all fragment template paths from the embedded FS. 102func (p *Pages) FragmentPaths() ([]string, error) { 103 return p.fragmentPaths() 104} 105 106// EmbedFS returns the embedded filesystem containing templates and static assets. 107func (p *Pages) EmbedFS() fs.FS { 108 return p.embedFS 109} 110 111// ParseWith parses the base layout together with all appview fragments and 112// an additional template from extraFS identified by extraPath (relative to 113// extraFS root). The returned template is ready to ExecuteTemplate with 114// "layouts/base" -- primarily for use with the blog. 115func (p *Pages) ParseWith(extraFS fs.FS, extraPath string) (*template.Template, error) { 116 fragmentPaths, err := p.fragmentPaths() 117 if err != nil { 118 return nil, err 119 } 120 121 funcs := p.funcMap() 122 tpl, err := template.New("layouts/base"). 123 Funcs(funcs). 124 ParseFS(p.embedFS, append(fragmentPaths, p.nameToPath("layouts/base"))...) 125 if err != nil { 126 return nil, err 127 } 128 129 err = fs.WalkDir(extraFS, ".", func(path string, d fs.DirEntry, err error) error { 130 if err != nil { 131 return err 132 } 133 if d.IsDir() || !strings.HasSuffix(path, ".html") { 134 return nil 135 } 136 if path != extraPath && !strings.Contains(path, "fragments/") { 137 return nil 138 } 139 data, err := fs.ReadFile(extraFS, path) 140 if err != nil { 141 return err 142 } 143 if _, err = tpl.New(path).Parse(string(data)); err != nil { 144 return err 145 } 146 return nil 147 }) 148 if err != nil { 149 return nil, err 150 } 151 152 return tpl, nil 153} 154 155func (p *Pages) fragmentPaths() ([]string, error) { 156 var fragmentPaths []string 157 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 158 if err != nil { 159 return err 160 } 161 if d.IsDir() { 162 return nil 163 } 164 if !strings.HasSuffix(path, ".html") { 165 return nil 166 } 167 if !strings.Contains(path, "fragments/") { 168 return nil 169 } 170 fragmentPaths = append(fragmentPaths, path) 171 return nil 172 }) 173 if err != nil { 174 return nil, err 175 } 176 177 return fragmentPaths, nil 178} 179 180// parse without memoization 181func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 182 paths, err := p.fragmentPaths() 183 if err != nil { 184 return nil, err 185 } 186 for _, s := range stack { 187 paths = append(paths, p.nameToPath(s)) 188 } 189 190 funcs := p.funcMap() 191 top := stack[len(stack)-1] 192 parsed, err := template.New(top). 193 Funcs(funcs). 194 ParseFS(p.embedFS, paths...) 195 if err != nil { 196 return nil, err 197 } 198 199 return parsed, nil 200} 201 202func (p *Pages) parse(stack ...string) (*template.Template, error) { 203 key := strings.Join(stack, "|") 204 205 // never cache in dev mode 206 if cached, exists := p.cache.Get(key); !p.dev && exists { 207 return cached, nil 208 } 209 210 result, err := p.rawParse(stack...) 211 if err != nil { 212 return nil, err 213 } 214 215 p.cache.Set(key, result) 216 return result, nil 217} 218 219func (p *Pages) parseBase(top string) (*template.Template, error) { 220 stack := []string{ 221 "layouts/base", 222 top, 223 } 224 return p.parse(stack...) 225} 226 227func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 228 stack := []string{ 229 "layouts/base", 230 "layouts/repobase", 231 top, 232 } 233 return p.parse(stack...) 234} 235 236func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 237 stack := []string{ 238 "layouts/base", 239 "layouts/profilebase", 240 top, 241 } 242 return p.parse(stack...) 243} 244 245func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 246 stack := []string{ 247 "layouts/base", 248 "layouts/loginbase", 249 top, 250 } 251 return p.parse(stack...) 252} 253 254func (p *Pages) executePlain(name string, w io.Writer, params any) error { 255 tpl, err := p.parse(name) 256 if err != nil { 257 return err 258 } 259 260 return tpl.Execute(w, params) 261} 262 263func (p *Pages) executeLogin(name string, w io.Writer, params any) error { 264 tpl, err := p.parseLoginBase(name) 265 if err != nil { 266 return err 267 } 268 269 return tpl.ExecuteTemplate(w, "layouts/base", params) 270} 271 272func (p *Pages) execute(name string, w io.Writer, params any) error { 273 tpl, err := p.parseBase(name) 274 if err != nil { 275 return err 276 } 277 278 return tpl.ExecuteTemplate(w, "layouts/base", params) 279} 280 281func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 282 tpl, err := p.parseRepoBase(name) 283 if err != nil { 284 return err 285 } 286 287 return tpl.ExecuteTemplate(w, "layouts/base", params) 288} 289 290func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 291 tpl, err := p.parseProfileBase(name) 292 if err != nil { 293 return err 294 } 295 296 return tpl.ExecuteTemplate(w, "layouts/base", params) 297} 298 299type DollyParams struct { 300 Classes string 301 FillColor string 302} 303 304func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 305 return p.executePlain("fragments/dolly/logo", w, params) 306} 307 308func (p *Pages) Favicon(w io.Writer) error { 309 return p.Dolly(w, DollyParams{ 310 Classes: "text-black dark:text-white", 311 }) 312} 313 314type LoginParams struct { 315 ReturnUrl string 316 ErrorCode string 317 AddAccount bool 318 Accounts []oauth.AccountInfo 319} 320 321func (p *Pages) Login(w io.Writer, params LoginParams) error { 322 return p.executeLogin("user/login", w, params) 323} 324 325type SignupParams struct { 326 CloudflareSiteKey string 327 EmailId string 328} 329 330func (p *Pages) Signup(w io.Writer, params SignupParams) error { 331 return p.executeLogin("user/signup", w, params) 332} 333 334func (p *Pages) CompleteSignup(w io.Writer) error { 335 return p.executeLogin("user/completeSignup", w, nil) 336} 337 338type TermsOfServiceParams struct { 339 LoggedInUser *oauth.MultiAccountUser 340 Content template.HTML 341} 342 343func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 344 filename := "terms.md" 345 filePath := filepath.Join("legal", filename) 346 347 file, err := p.embedFS.Open(filePath) 348 if err != nil { 349 return fmt.Errorf("failed to read %s: %w", filename, err) 350 } 351 defer file.Close() 352 353 markdownBytes, err := io.ReadAll(file) 354 if err != nil { 355 return fmt.Errorf("failed to read %s: %w", filename, err) 356 } 357 358 rctx := p.rctx.Clone() 359 rctx.RendererType = markup.RendererTypeDefault 360 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 361 sanitized := rctx.SanitizeDefault(htmlString) 362 params.Content = template.HTML(sanitized) 363 364 return p.execute("legal/terms", w, params) 365} 366 367type PrivacyPolicyParams struct { 368 LoggedInUser *oauth.MultiAccountUser 369 Content template.HTML 370} 371 372func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 373 filename := "privacy.md" 374 filePath := filepath.Join("legal", filename) 375 376 file, err := p.embedFS.Open(filePath) 377 if err != nil { 378 return fmt.Errorf("failed to read %s: %w", filename, err) 379 } 380 defer file.Close() 381 382 markdownBytes, err := io.ReadAll(file) 383 if err != nil { 384 return fmt.Errorf("failed to read %s: %w", filename, err) 385 } 386 387 rctx := p.rctx.Clone() 388 rctx.RendererType = markup.RendererTypeDefault 389 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 390 sanitized := rctx.SanitizeDefault(htmlString) 391 params.Content = template.HTML(sanitized) 392 393 return p.execute("legal/privacy", w, params) 394} 395 396type BrandParams struct { 397 LoggedInUser *oauth.MultiAccountUser 398} 399 400func (p *Pages) Brand(w io.Writer, params BrandParams) error { 401 return p.execute("brand/brand", w, params) 402} 403 404type TimelineParams struct { 405 LoggedInUser *oauth.MultiAccountUser 406 Timeline []models.TimelineEvent 407 Repos []models.Repo 408 GfiLabel *models.LabelDefinition 409 BlueskyPosts []models.BskyPost 410 // ShowNewsletter controls whether the newsletter widget/CTA is rendered. 411 // For logged-in users it reflects their newsletter_preferences row; for 412 // anonymous visitors it is always true (dismissal falls back to 413 // localStorage on the client). 414 ShowNewsletter bool 415} 416 417func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 418 return p.execute("timeline/timeline", w, params) 419} 420 421type GoodFirstIssuesParams struct { 422 LoggedInUser *oauth.MultiAccountUser 423 Issues []models.Issue 424 RepoGroups []*models.RepoGroup 425 LabelDefs map[string]*models.LabelDefinition 426 GfiLabel *models.LabelDefinition 427 Page pagination.Page 428} 429 430func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 431 return p.execute("goodfirstissues/index", w, params) 432} 433 434type UserProfileSettingsParams struct { 435 LoggedInUser *oauth.MultiAccountUser 436 Tab string 437 PunchcardPreference models.PunchcardPreference 438 IsTnglSh bool 439 IsDeactivated bool 440 HandleOpen bool 441} 442 443func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 444 params.Tab = "profile" 445 return p.execute("user/settings/profile", w, params) 446} 447 448type NotificationsParams struct { 449 LoggedInUser *oauth.MultiAccountUser 450 Notifications []*models.NotificationWithEntity 451 UnreadCount int 452 Page pagination.Page 453 Total int64 454} 455 456func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 457 return p.execute("notifications/list", w, params) 458} 459 460type NotificationItemParams struct { 461 Notification *models.Notification 462} 463 464func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 465 return p.executePlain("notifications/fragments/item", w, params) 466} 467 468type NotificationCountParams struct { 469 Count int64 470} 471 472func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 473 return p.executePlain("notifications/fragments/count", w, params) 474} 475 476type UserKeysSettingsParams struct { 477 LoggedInUser *oauth.MultiAccountUser 478 PubKeys []models.PublicKey 479 Tab string 480} 481 482func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 483 params.Tab = "keys" 484 return p.execute("user/settings/keys", w, params) 485} 486 487type UserEmailsSettingsParams struct { 488 LoggedInUser *oauth.MultiAccountUser 489 Emails []models.Email 490 Tab string 491} 492 493func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 494 params.Tab = "emails" 495 return p.execute("user/settings/emails", w, params) 496} 497 498type UserNotificationSettingsParams struct { 499 LoggedInUser *oauth.MultiAccountUser 500 Preferences *models.NotificationPreferences 501 Tab string 502} 503 504func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 505 params.Tab = "notifications" 506 return p.execute("user/settings/notifications", w, params) 507} 508 509type UserSiteSettingsParams struct { 510 LoggedInUser *oauth.MultiAccountUser 511 Claim *models.DomainClaim 512 SitesDomain string 513 IsTnglHandle bool 514 Tab string 515} 516 517func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error { 518 params.Tab = "sites" 519 return p.execute("user/settings/sites", w, params) 520} 521 522type UpgradeBannerParams struct { 523 Registrations []models.Registration 524 Spindles []models.Spindle 525} 526 527func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 528 return p.executePlain("banner", w, params) 529} 530 531type NewsletterResponseParams struct { 532 // Id identifies the calling form instance; the response span's id will 533 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target. 534 Id string 535 // Error, when non-empty, switches the template to the error variant. 536 Error string 537} 538 539func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error { 540 return p.executePlain("timeline/fragments/newsletterResponse", w, params) 541} 542 543type KnotsParams struct { 544 LoggedInUser *oauth.MultiAccountUser 545 Registrations []models.Registration 546 Tab string 547} 548 549func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 550 params.Tab = "knots" 551 return p.execute("knots/index", w, params) 552} 553 554type KnotParams struct { 555 LoggedInUser *oauth.MultiAccountUser 556 Registration *models.Registration 557 Members []string 558 Repos map[string][]models.Repo 559 IsOwner bool 560 Tab string 561} 562 563func (p *Pages) Knot(w io.Writer, params KnotParams) error { 564 return p.execute("knots/dashboard", w, params) 565} 566 567type KnotListingParams struct { 568 *models.Registration 569} 570 571func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 572 return p.executePlain("knots/fragments/knotListing", w, params) 573} 574 575type SpindlesParams struct { 576 LoggedInUser *oauth.MultiAccountUser 577 Spindles []models.Spindle 578 Tab string 579} 580 581func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 582 params.Tab = "spindles" 583 return p.execute("spindles/index", w, params) 584} 585 586type SpindleListingParams struct { 587 models.Spindle 588 Tab string 589} 590 591func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 592 return p.executePlain("spindles/fragments/spindleListing", w, params) 593} 594 595type SpindleDashboardParams struct { 596 LoggedInUser *oauth.MultiAccountUser 597 Spindle models.Spindle 598 Members []string 599 Repos map[string][]models.Repo 600 Tab string 601} 602 603func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 604 return p.execute("spindles/dashboard", w, params) 605} 606 607type NewRepoParams struct { 608 LoggedInUser *oauth.MultiAccountUser 609 Knots []string 610} 611 612func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 613 return p.execute("repo/new", w, params) 614} 615 616type ForkRepoParams struct { 617 LoggedInUser *oauth.MultiAccountUser 618 Knots []string 619 RepoInfo repoinfo.RepoInfo 620} 621 622func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 623 return p.execute("repo/fork", w, params) 624} 625 626type ProfileCard struct { 627 UserDid string 628 HasProfile bool 629 FollowStatus models.FollowStatus 630 VouchRelationship *models.VouchRelationship 631 Punchcard *models.Punchcard 632 Profile *models.Profile 633 Stats ProfileStats 634 Active string 635} 636 637type ProfileStats struct { 638 RepoCount int64 639 StarredCount int64 640 StringCount int64 641 FollowersCount int64 642 FollowingCount int64 643} 644 645func (p *ProfileCard) GetTabs() [][]any { 646 tabs := [][]any{ 647 {"overview", "overview", "square-chart-gantt", nil}, 648 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 649 {"starred", "starred", "star", p.Stats.StarredCount}, 650 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 651 {"vouches", "vouches", "shield", nil}, 652 } 653 654 return tabs 655} 656 657type ProfileOverviewParams struct { 658 LoggedInUser *oauth.MultiAccountUser 659 Repos []models.Repo 660 CollaboratingRepos []models.Repo 661 ProfileTimeline *models.ProfileTimeline 662 Card *ProfileCard 663 Active string 664 ShowPunchcard bool 665} 666 667func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 668 params.Active = "overview" 669 return p.executeProfile("user/overview", w, params) 670} 671 672type ProfileReposParams struct { 673 LoggedInUser *oauth.MultiAccountUser 674 Repos []models.Repo 675 Card *ProfileCard 676 Active string 677 Page pagination.Page 678 RepoCount int 679 FilterQuery string 680} 681 682func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 683 params.Active = "repos" 684 return p.executeProfile("user/repos", w, params) 685} 686 687type ProfileStarredParams struct { 688 LoggedInUser *oauth.MultiAccountUser 689 Repos []models.Repo 690 Card *ProfileCard 691 Page pagination.Page 692 Total int 693 Active string 694} 695 696func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 697 params.Active = "starred" 698 return p.executeProfile("user/starred", w, params) 699} 700 701type ProfileStringsParams struct { 702 LoggedInUser *oauth.MultiAccountUser 703 Strings []models.String 704 Card *ProfileCard 705 Active string 706} 707 708func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 709 params.Active = "strings" 710 return p.executeProfile("user/strings", w, params) 711} 712 713type ProfileVouchesParams struct { 714 LoggedInUser *oauth.MultiAccountUser 715 Vouches []models.Vouch 716 Suggestions []models.VouchSuggestion 717 Card *ProfileCard 718 Page pagination.Page 719 Active string 720} 721 722func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { 723 params.Active = "vouches" 724 return p.executeProfile("user/vouches", w, params) 725} 726 727type FollowCard struct { 728 UserDid string 729 LoggedInUser *oauth.MultiAccountUser 730 FollowStatus models.FollowStatus 731 FollowersCount int64 732 FollowingCount int64 733 Profile *models.Profile 734} 735 736type ProfileFollowersParams struct { 737 LoggedInUser *oauth.MultiAccountUser 738 Followers []FollowCard 739 Card *ProfileCard 740 Active string 741} 742 743func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 744 params.Active = "overview" 745 return p.executeProfile("user/followers", w, params) 746} 747 748type ProfileFollowingParams struct { 749 LoggedInUser *oauth.MultiAccountUser 750 Following []FollowCard 751 Card *ProfileCard 752 Active string 753} 754 755func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 756 params.Active = "overview" 757 return p.executeProfile("user/following", w, params) 758} 759 760type FollowFragmentParams struct { 761 UserDid string 762 FollowStatus models.FollowStatus 763 FollowersCount int64 764} 765 766func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 767 return p.executePlain("user/fragments/follow-oob", w, params) 768} 769 770type EditBioParams struct { 771 LoggedInUser *oauth.MultiAccountUser 772 Profile *models.Profile 773 AlsoKnownAs []string 774} 775 776func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 777 return p.executePlain("user/fragments/editBio", w, params) 778} 779 780type EditPinsParams struct { 781 LoggedInUser *oauth.MultiAccountUser 782 Profile *models.Profile 783 AllRepos []PinnedRepo 784} 785 786type PinnedRepo struct { 787 IsPinned bool 788 models.Repo 789} 790 791func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 792 return p.executePlain("user/fragments/editPins", w, params) 793} 794 795type StarBtnFragmentParams struct { 796 IsStarred bool 797 SubjectAt syntax.ATURI 798 StarCount int 799 RepoName string 800 HxSwapOob bool 801} 802 803func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 804 params.HxSwapOob = true 805 return p.executePlain("fragments/starBtn", w, params) 806} 807 808type RepoIndexParams struct { 809 LoggedInUser *oauth.MultiAccountUser 810 RepoInfo repoinfo.RepoInfo 811 Active string 812 TagMap map[string][]string 813 CommitsTrunc []types.Commit 814 TagsTrunc []*types.TagReference 815 BranchesTrunc []types.Branch 816 // ForkInfo *types.ForkInfo 817 HTMLReadme template.HTML 818 Raw bool 819 EmailToDid map[string]string 820 VerifiedCommits commitverify.VerifiedCommits 821 Languages []types.RepoLanguageDetails 822 Pipelines map[string]models.Pipeline 823 NeedsKnotUpgrade bool 824 KnotUnreachable bool 825 types.RepoIndexResponse 826} 827 828func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 829 params.Active = "overview" 830 if params.IsEmpty { 831 return p.executeRepo("repo/empty", w, params) 832 } 833 834 if params.NeedsKnotUpgrade { 835 return p.executeRepo("repo/needsUpgrade", w, params) 836 } 837 838 if params.KnotUnreachable { 839 return p.executeRepo("repo/knotUnreachable", w, params) 840 } 841 842 rctx := p.rctx.Clone() 843 rctx.RepoInfo = params.RepoInfo 844 rctx.RepoInfo.Ref = params.Ref 845 rctx.RendererType = markup.RendererTypeRepoMarkdown 846 847 if params.ReadmeFileName != "" { 848 ext := filepath.Ext(params.ReadmeFileName) 849 switch ext { 850 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 851 params.Raw = false 852 htmlString := rctx.RenderMarkdown(params.Readme) 853 sanitized := rctx.SanitizeDefault(htmlString) 854 params.HTMLReadme = template.HTML(sanitized) 855 default: 856 params.Raw = true 857 } 858 } 859 860 return p.executeRepo("repo/index", w, params) 861} 862 863type RepoLogParams struct { 864 LoggedInUser *oauth.MultiAccountUser 865 RepoInfo repoinfo.RepoInfo 866 TagMap map[string][]string 867 Active string 868 EmailToDid map[string]string 869 VerifiedCommits commitverify.VerifiedCommits 870 Pipelines map[string]models.Pipeline 871 872 types.RepoLogResponse 873} 874 875func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 876 params.Active = "overview" 877 return p.executeRepo("repo/log", w, params) 878} 879 880type RepoCommitParams struct { 881 LoggedInUser *oauth.MultiAccountUser 882 RepoInfo repoinfo.RepoInfo 883 Active string 884 EmailToDid map[string]string 885 Pipeline *models.Pipeline 886 DiffOpts types.DiffOpts 887 888 // singular because it's always going to be just one 889 VerifiedCommit commitverify.VerifiedCommits 890 891 types.RepoCommitResponse 892} 893 894func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 895 params.Active = "overview" 896 return p.executeRepo("repo/commit", w, params) 897} 898 899type RepoTreeParams struct { 900 LoggedInUser *oauth.MultiAccountUser 901 RepoInfo repoinfo.RepoInfo 902 Active string 903 BreadCrumbs [][]string 904 Path string 905 Raw bool 906 HTMLReadme template.HTML 907 EmailToDid map[string]string 908 LastCommitInfo *types.LastCommitInfo 909 types.RepoTreeResponse 910} 911 912type RepoTreeStats struct { 913 NumFolders uint64 914 NumFiles uint64 915} 916 917func (r RepoTreeParams) TreeStats() RepoTreeStats { 918 numFolders, numFiles := 0, 0 919 for _, f := range r.Files { 920 if !f.IsFile() { 921 numFolders += 1 922 } else if f.IsFile() { 923 numFiles += 1 924 } 925 } 926 927 return RepoTreeStats{ 928 NumFolders: uint64(numFolders), 929 NumFiles: uint64(numFiles), 930 } 931} 932 933func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 934 params.Active = "overview" 935 936 rctx := p.rctx.Clone() 937 rctx.RepoInfo = params.RepoInfo 938 rctx.RepoInfo.Ref = params.Ref 939 rctx.RendererType = markup.RendererTypeRepoMarkdown 940 941 if params.ReadmeFileName != "" { 942 ext := filepath.Ext(params.ReadmeFileName) 943 switch ext { 944 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 945 params.Raw = false 946 htmlString := rctx.RenderMarkdown(params.Readme) 947 sanitized := rctx.SanitizeDefault(htmlString) 948 params.HTMLReadme = template.HTML(sanitized) 949 default: 950 params.Raw = true 951 } 952 } 953 954 return p.executeRepo("repo/tree", w, params) 955} 956 957type RepoBranchesParams struct { 958 LoggedInUser *oauth.MultiAccountUser 959 RepoInfo repoinfo.RepoInfo 960 Active string 961 types.RepoBranchesResponse 962} 963 964func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 965 params.Active = "overview" 966 return p.executeRepo("repo/branches", w, params) 967} 968 969type RepoTagsParams struct { 970 LoggedInUser *oauth.MultiAccountUser 971 RepoInfo repoinfo.RepoInfo 972 Active string 973 types.RepoTagsResponse 974 ArtifactMap map[plumbing.Hash][]models.Artifact 975 DanglingArtifacts []models.Artifact 976} 977 978func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 979 params.Active = "overview" 980 return p.executeRepo("repo/tags", w, params) 981} 982 983type RepoTagParams struct { 984 LoggedInUser *oauth.MultiAccountUser 985 RepoInfo repoinfo.RepoInfo 986 Active string 987 types.RepoTagResponse 988 ArtifactMap map[plumbing.Hash][]models.Artifact 989 DanglingArtifacts []models.Artifact 990} 991 992func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 993 params.Active = "overview" 994 return p.executeRepo("repo/tag", w, params) 995} 996 997type RepoArtifactParams struct { 998 LoggedInUser *oauth.MultiAccountUser 999 RepoInfo repoinfo.RepoInfo 1000 Artifact models.Artifact 1001} 1002 1003func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1004 return p.executePlain("repo/fragments/artifact", w, params) 1005} 1006 1007type RepoBlobParams struct { 1008 LoggedInUser *oauth.MultiAccountUser 1009 RepoInfo repoinfo.RepoInfo 1010 Active string 1011 BreadCrumbs [][]string 1012 BlobView models.BlobView 1013 EmailToDid map[string]string 1014 LastCommitInfo *types.LastCommitInfo 1015 *tangled.RepoBlob_Output 1016} 1017 1018func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1019 params.Active = "overview" 1020 return p.executeRepo("repo/blob", w, params) 1021} 1022 1023type Collaborator struct { 1024 Did string 1025 Role string 1026} 1027 1028type RepoSettingsParams struct { 1029 LoggedInUser *oauth.MultiAccountUser 1030 RepoInfo repoinfo.RepoInfo 1031 Collaborators []Collaborator 1032 Active string 1033 Branches []types.Branch 1034 Spindles []string 1035 CurrentSpindle string 1036 Secrets []*tangled.RepoListSecrets_Secret 1037 1038 // TODO: use repoinfo.roles 1039 IsCollaboratorInviteAllowed bool 1040} 1041 1042func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1043 params.Active = "settings" 1044 return p.executeRepo("repo/settings", w, params) 1045} 1046 1047type RepoGeneralSettingsParams struct { 1048 LoggedInUser *oauth.MultiAccountUser 1049 RepoInfo repoinfo.RepoInfo 1050 Labels []models.LabelDefinition 1051 DefaultLabels []models.LabelDefinition 1052 SubscribedLabels map[string]struct{} 1053 ShouldSubscribeAll bool 1054 Active string 1055 Tab string 1056 Branches []types.Branch 1057} 1058 1059func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1060 params.Active = "settings" 1061 params.Tab = "general" 1062 return p.executeRepo("repo/settings/general", w, params) 1063} 1064 1065type RepoAccessSettingsParams struct { 1066 LoggedInUser *oauth.MultiAccountUser 1067 RepoInfo repoinfo.RepoInfo 1068 Active string 1069 Tab string 1070 Collaborators []Collaborator 1071} 1072 1073func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1074 params.Active = "settings" 1075 params.Tab = "access" 1076 return p.executeRepo("repo/settings/access", w, params) 1077} 1078 1079type RepoPipelineSettingsParams struct { 1080 LoggedInUser *oauth.MultiAccountUser 1081 RepoInfo repoinfo.RepoInfo 1082 Active string 1083 Tab string 1084 Spindles []string 1085 CurrentSpindle string 1086 Secrets []map[string]any 1087} 1088 1089func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1090 params.Active = "settings" 1091 params.Tab = "pipelines" 1092 return p.executeRepo("repo/settings/pipelines", w, params) 1093} 1094 1095type RepoWebhooksSettingsParams struct { 1096 LoggedInUser *oauth.MultiAccountUser 1097 RepoInfo repoinfo.RepoInfo 1098 Active string 1099 Tab string 1100 Webhooks []models.Webhook 1101 WebhookDeliveries map[int64][]models.WebhookDelivery 1102} 1103 1104func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1105 params.Active = "settings" 1106 params.Tab = "hooks" 1107 return p.executeRepo("repo/settings/hooks", w, params) 1108} 1109 1110type WebhookDeliveriesListParams struct { 1111 LoggedInUser *oauth.MultiAccountUser 1112 RepoInfo repoinfo.RepoInfo 1113 Webhook *models.Webhook 1114 Deliveries []models.WebhookDelivery 1115} 1116 1117func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1118 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1119 if err != nil { 1120 return err 1121 } 1122 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1123} 1124 1125type RepoSiteSettingsParams struct { 1126 LoggedInUser *oauth.MultiAccountUser 1127 RepoInfo repoinfo.RepoInfo 1128 Active string 1129 Tab string 1130 Branches []types.Branch 1131 SiteConfig *models.RepoSite 1132 OwnerClaim *models.DomainClaim 1133 Deploys []models.SiteDeploy 1134 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1135} 1136 1137func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1138 params.Active = "settings" 1139 params.Tab = "sites" 1140 return p.executeRepo("repo/settings/sites", w, params) 1141} 1142 1143type RepoIssuesParams struct { 1144 LoggedInUser *oauth.MultiAccountUser 1145 RepoInfo repoinfo.RepoInfo 1146 Active string 1147 Issues []models.Issue 1148 IssueCount int 1149 LabelDefs map[string]*models.LabelDefinition 1150 Page pagination.Page 1151 FilterState string 1152 FilterQuery string 1153 VouchRelationships map[syntax.DID]*models.VouchRelationship 1154} 1155 1156func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1157 params.Active = "issues" 1158 return p.executeRepo("repo/issues/issues", w, params) 1159} 1160 1161type RepoSingleIssueParams struct { 1162 LoggedInUser *oauth.MultiAccountUser 1163 RepoInfo repoinfo.RepoInfo 1164 Active string 1165 Issue *models.Issue 1166 CommentList []models.CommentListItem 1167 Backlinks []models.RichReferenceLink 1168 LabelDefs map[string]*models.LabelDefinition 1169 1170 Reactions map[models.ReactionKind]models.ReactionDisplayData 1171 UserReacted map[models.ReactionKind]bool 1172 VouchRelationships map[syntax.DID]*models.VouchRelationship 1173} 1174 1175func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1176 params.Active = "issues" 1177 return p.executeRepo("repo/issues/issue", w, params) 1178} 1179 1180type EditIssueParams struct { 1181 LoggedInUser *oauth.MultiAccountUser 1182 RepoInfo repoinfo.RepoInfo 1183 Issue *models.Issue 1184 Action string 1185} 1186 1187func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1188 params.Action = "edit" 1189 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1190} 1191 1192type ThreadReactionFragmentParams struct { 1193 ThreadAt syntax.ATURI 1194 Kind models.ReactionKind 1195 Count int 1196 Users []string 1197 IsReacted bool 1198} 1199 1200func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1201 return p.executePlain("repo/fragments/reaction", w, params) 1202} 1203 1204type RepoNewIssueParams struct { 1205 LoggedInUser *oauth.MultiAccountUser 1206 RepoInfo repoinfo.RepoInfo 1207 Issue *models.Issue // existing issue if any -- passed when editing 1208 Active string 1209 Action string 1210} 1211 1212func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1213 params.Active = "issues" 1214 params.Action = "create" 1215 return p.executeRepo("repo/issues/new", w, params) 1216} 1217 1218type EditIssueCommentParams struct { 1219 LoggedInUser *oauth.MultiAccountUser 1220 RepoInfo repoinfo.RepoInfo 1221 Issue *models.Issue 1222 Comment *models.IssueComment 1223} 1224 1225func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1226 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1227} 1228 1229type ReplyIssueCommentPlaceholderParams struct { 1230 LoggedInUser *oauth.MultiAccountUser 1231 RepoInfo repoinfo.RepoInfo 1232 Issue *models.Issue 1233 Comment *models.IssueComment 1234} 1235 1236func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1237 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1238} 1239 1240type ReplyIssueCommentParams struct { 1241 LoggedInUser *oauth.MultiAccountUser 1242 RepoInfo repoinfo.RepoInfo 1243 Issue *models.Issue 1244 Comment *models.IssueComment 1245} 1246 1247func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1248 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1249} 1250 1251type IssueCommentBodyParams struct { 1252 LoggedInUser *oauth.MultiAccountUser 1253 RepoInfo repoinfo.RepoInfo 1254 Issue *models.Issue 1255 Comment *models.IssueComment 1256} 1257 1258func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1259 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1260} 1261 1262type RepoNewPullParams struct { 1263 LoggedInUser *oauth.MultiAccountUser 1264 RepoInfo repoinfo.RepoInfo 1265 Branches []types.Branch 1266 Strategy string 1267 SourceBranch string 1268 TargetBranch string 1269 Title string 1270 Body string 1271 Active string 1272} 1273 1274func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1275 params.Active = "pulls" 1276 return p.executeRepo("repo/pulls/new", w, params) 1277} 1278 1279type RepoPullsParams struct { 1280 LoggedInUser *oauth.MultiAccountUser 1281 RepoInfo repoinfo.RepoInfo 1282 Pulls []*models.Pull 1283 Active string 1284 FilterState string 1285 FilterQuery string 1286 Stacks []models.Stack 1287 Pipelines map[string]models.Pipeline 1288 LabelDefs map[string]*models.LabelDefinition 1289 Page pagination.Page 1290 PullCount int 1291 VouchRelationships map[syntax.DID]*models.VouchRelationship 1292} 1293 1294func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1295 params.Active = "pulls" 1296 return p.executeRepo("repo/pulls/pulls", w, params) 1297} 1298 1299type ResubmitResult uint64 1300 1301const ( 1302 ShouldResubmit ResubmitResult = iota 1303 ShouldNotResubmit 1304 Unknown 1305) 1306 1307func (r ResubmitResult) Yes() bool { 1308 return r == ShouldResubmit 1309} 1310func (r ResubmitResult) No() bool { 1311 return r == ShouldNotResubmit 1312} 1313func (r ResubmitResult) Unknown() bool { 1314 return r == Unknown 1315} 1316 1317type RepoSinglePullParams struct { 1318 LoggedInUser *oauth.MultiAccountUser 1319 RepoInfo repoinfo.RepoInfo 1320 Active string 1321 Pull *models.Pull 1322 Stack models.Stack 1323 Backlinks []models.RichReferenceLink 1324 BranchDeleteStatus *models.BranchDeleteStatus 1325 MergeCheck types.MergeCheckResponse 1326 ResubmitCheck ResubmitResult 1327 Pipelines map[string]models.Pipeline 1328 Diff types.DiffRenderer 1329 DiffOpts types.DiffOpts 1330 ActiveRound int 1331 IsInterdiff bool 1332 1333 Reactions map[models.ReactionKind]models.ReactionDisplayData 1334 UserReacted map[models.ReactionKind]bool 1335 1336 LabelDefs map[string]*models.LabelDefinition 1337 VouchRelationships map[syntax.DID]*models.VouchRelationship 1338} 1339 1340func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1341 params.Active = "pulls" 1342 return p.executeRepo("repo/pulls/pull", w, params) 1343} 1344 1345type RepoPullPatchParams struct { 1346 LoggedInUser *oauth.MultiAccountUser 1347 RepoInfo repoinfo.RepoInfo 1348 Pull *models.Pull 1349 Stack models.Stack 1350 Diff *types.NiceDiff 1351 Round int 1352 Submission *models.PullSubmission 1353 DiffOpts types.DiffOpts 1354} 1355 1356// this name is a mouthful 1357func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1358 return p.execute("repo/pulls/patch", w, params) 1359} 1360 1361type RepoPullInterdiffParams struct { 1362 LoggedInUser *oauth.MultiAccountUser 1363 RepoInfo repoinfo.RepoInfo 1364 Pull *models.Pull 1365 Round int 1366 Interdiff *patchutil.InterdiffResult 1367 DiffOpts types.DiffOpts 1368} 1369 1370// this name is a mouthful 1371func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1372 return p.execute("repo/pulls/interdiff", w, params) 1373} 1374 1375type PullPatchUploadParams struct { 1376 RepoInfo repoinfo.RepoInfo 1377} 1378 1379func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1380 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1381} 1382 1383type PullCompareBranchesParams struct { 1384 RepoInfo repoinfo.RepoInfo 1385 Branches []types.Branch 1386 SourceBranch string 1387} 1388 1389func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1390 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1391} 1392 1393type PullCompareForkParams struct { 1394 RepoInfo repoinfo.RepoInfo 1395 Forks []models.Repo 1396 Selected string 1397} 1398 1399func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1400 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1401} 1402 1403type PullCompareForkBranchesParams struct { 1404 RepoInfo repoinfo.RepoInfo 1405 SourceBranches []types.Branch 1406 TargetBranches []types.Branch 1407} 1408 1409func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1410 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1411} 1412 1413type PullResubmitParams struct { 1414 LoggedInUser *oauth.MultiAccountUser 1415 RepoInfo repoinfo.RepoInfo 1416 Pull *models.Pull 1417 SubmissionId int 1418} 1419 1420func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1421 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1422} 1423 1424type PullActionsParams struct { 1425 LoggedInUser *oauth.MultiAccountUser 1426 RepoInfo repoinfo.RepoInfo 1427 Pull *models.Pull 1428 RoundNumber int 1429 MergeCheck types.MergeCheckResponse 1430 ResubmitCheck ResubmitResult 1431 BranchDeleteStatus *models.BranchDeleteStatus 1432 Stack models.Stack 1433} 1434 1435func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1436 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1437} 1438 1439type PullNewCommentParams struct { 1440 LoggedInUser *oauth.MultiAccountUser 1441 RepoInfo repoinfo.RepoInfo 1442 Pull *models.Pull 1443 RoundNumber int 1444} 1445 1446func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1447 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1448} 1449 1450type RepoCompareParams struct { 1451 LoggedInUser *oauth.MultiAccountUser 1452 RepoInfo repoinfo.RepoInfo 1453 Forks []models.Repo 1454 Branches []types.Branch 1455 Tags []*types.TagReference 1456 Base string 1457 Head string 1458 Diff *types.NiceDiff 1459 DiffOpts types.DiffOpts 1460 1461 Active string 1462} 1463 1464func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1465 params.Active = "overview" 1466 return p.executeRepo("repo/compare/compare", w, params) 1467} 1468 1469type RepoCompareNewParams struct { 1470 LoggedInUser *oauth.MultiAccountUser 1471 RepoInfo repoinfo.RepoInfo 1472 Forks []models.Repo 1473 Branches []types.Branch 1474 Tags []*types.TagReference 1475 Base string 1476 Head string 1477 1478 Active string 1479} 1480 1481func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1482 params.Active = "overview" 1483 return p.executeRepo("repo/compare/new", w, params) 1484} 1485 1486type RepoCompareAllowPullParams struct { 1487 LoggedInUser *oauth.MultiAccountUser 1488 RepoInfo repoinfo.RepoInfo 1489 Base string 1490 Head string 1491} 1492 1493func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1494 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1495} 1496 1497type RepoCompareDiffFragmentParams struct { 1498 Diff types.NiceDiff 1499 DiffOpts types.DiffOpts 1500} 1501 1502func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1503 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1504} 1505 1506type LabelPanelParams struct { 1507 LoggedInUser *oauth.MultiAccountUser 1508 RepoInfo repoinfo.RepoInfo 1509 Defs map[string]*models.LabelDefinition 1510 Subject string 1511 State models.LabelState 1512} 1513 1514func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1515 return p.executePlain("repo/fragments/labelPanel", w, params) 1516} 1517 1518type EditLabelPanelParams struct { 1519 LoggedInUser *oauth.MultiAccountUser 1520 RepoInfo repoinfo.RepoInfo 1521 Defs map[string]*models.LabelDefinition 1522 Subject string 1523 State models.LabelState 1524} 1525 1526func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1527 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1528} 1529 1530type RepoStarsParams struct { 1531 LoggedInUser *oauth.MultiAccountUser 1532 RepoInfo repoinfo.RepoInfo 1533 Active string 1534 Starrers []models.Star 1535 Page pagination.Page 1536 TotalCount int 1537} 1538 1539func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1540 params.Active = "overview" 1541 return p.executeRepo("repo/stars", w, params) 1542} 1543 1544type PipelinesParams struct { 1545 LoggedInUser *oauth.MultiAccountUser 1546 RepoInfo repoinfo.RepoInfo 1547 Pipelines []models.Pipeline 1548 Active string 1549 FilterKind string 1550 Total int64 1551} 1552 1553func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1554 params.Active = "pipelines" 1555 return p.executeRepo("repo/pipelines/pipelines", w, params) 1556} 1557 1558type LogBlockParams struct { 1559 Id int 1560 Name string 1561 Command string 1562 Collapsed bool 1563 StartTime time.Time 1564} 1565 1566func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1567 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1568} 1569 1570type LogBlockEndParams struct { 1571 Id int 1572 StartTime time.Time 1573 EndTime time.Time 1574} 1575 1576func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1577 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1578} 1579 1580type LogLineParams struct { 1581 Id int 1582 Content string 1583} 1584 1585func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1586 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1587} 1588 1589type WorkflowParams struct { 1590 LoggedInUser *oauth.MultiAccountUser 1591 RepoInfo repoinfo.RepoInfo 1592 Pipeline models.Pipeline 1593 Workflow string 1594 LogUrl string 1595 Active string 1596} 1597 1598func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1599 params.Active = "pipelines" 1600 return p.executeRepo("repo/pipelines/workflow", w, params) 1601} 1602 1603type PutStringParams struct { 1604 LoggedInUser *oauth.MultiAccountUser 1605 Action string 1606 1607 // this is supplied in the case of editing an existing string 1608 String models.String 1609} 1610 1611func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1612 return p.execute("strings/put", w, params) 1613} 1614 1615type StringsDashboardParams struct { 1616 LoggedInUser *oauth.MultiAccountUser 1617 Card ProfileCard 1618 Strings []models.String 1619} 1620 1621func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1622 return p.execute("strings/dashboard", w, params) 1623} 1624 1625type StringTimelineParams struct { 1626 LoggedInUser *oauth.MultiAccountUser 1627 Strings []models.String 1628} 1629 1630func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1631 return p.execute("strings/timeline", w, params) 1632} 1633 1634type SingleStringParams struct { 1635 LoggedInUser *oauth.MultiAccountUser 1636 ShowRendered bool 1637 RenderToggle bool 1638 RenderedContents template.HTML 1639 String *models.String 1640 Stats models.StringStats 1641 IsStarred bool 1642 StarCount int 1643 Owner identity.Identity 1644} 1645 1646func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1647 return p.execute("strings/string", w, params) 1648} 1649 1650type SearchReposParams struct { 1651 LoggedInUser *oauth.MultiAccountUser 1652 Repos []models.Repo 1653 Page pagination.Page 1654 ResultCount int 1655 FilterQuery string 1656 SortParam string 1657 TimeTaken time.Duration 1658 DocCount int64 1659} 1660 1661func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1662 return p.execute("search/search", w, params) 1663} 1664 1665func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1666 return p.execute("timeline/home", w, params) 1667} 1668 1669func (p *Pages) Static() http.Handler { 1670 if p.dev { 1671 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1672 } 1673 1674 sub, err := fs.Sub(p.embedFS, "static") 1675 if err != nil { 1676 p.logger.Error("no static dir found? that's crazy", "err", err) 1677 panic(err) 1678 } 1679 // Custom handler to apply Cache-Control headers for font files 1680 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1681} 1682 1683func Cache(h http.Handler) http.Handler { 1684 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1685 path := strings.Split(r.URL.Path, "?")[0] 1686 1687 if strings.HasSuffix(path, ".css") { 1688 // on day for css files 1689 w.Header().Set("Cache-Control", "public, max-age=86400") 1690 } else { 1691 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1692 } 1693 h.ServeHTTP(w, r) 1694 }) 1695} 1696 1697func (p *Pages) CssContentHash() string { 1698 cssFile, err := p.embedFS.Open("static/tw.css") 1699 if err != nil { 1700 slog.Debug("Error opening CSS file", "err", err) 1701 return "" 1702 } 1703 defer cssFile.Close() 1704 1705 hasher := sha256.New() 1706 if _, err := io.Copy(hasher, cssFile); err != nil { 1707 slog.Debug("Error hashing CSS file", "err", err) 1708 return "" 1709 } 1710 1711 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1712} 1713 1714func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1715 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1716} 1717 1718func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1719 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1720} 1721 1722func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1723 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1724} 1725 1726func (p *Pages) Error500(w io.Writer) error { 1727 return p.execute("errors/500", w, nil) 1728} 1729 1730func (p *Pages) Error404(w io.Writer) error { 1731 return p.execute("errors/404", w, nil) 1732} 1733 1734func (p *Pages) ErrorKnot404(w io.Writer) error { 1735 return p.execute("errors/knot404", w, nil) 1736} 1737 1738func (p *Pages) Error503(w io.Writer) error { 1739 return p.execute("errors/503", w, nil) 1740}