forked from tangled.org/core
Monorepo for Tangled

appview: pages: initial htmxing of compare page

anirudh.fi c83c2259 abf6b940

verified
Changed files
+193 -92
appview
pages
templates
repo
state
+5 -6
appview/pages/pages.go
··· 863 863 Forks []db.Repo 864 864 Branches []types.Branch 865 865 Tags []*types.TagReference 866 + Base string 867 + Head string 866 868 867 869 Active string 868 870 } 869 871 870 872 func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 871 873 params.Active = "overview" 872 - return p.executeRepo("repo/compare/new", w, params) 874 + return p.executeRepo("repo/compare", w, params) 873 875 } 874 876 875 877 type RepoCompareDiffParams struct { 876 878 LoggedInUser *oauth.User 877 879 RepoInfo repoinfo.RepoInfo 878 - FormatPatch types.RepoFormatPatchResponse 879 - 880 - Active string 880 + Diff types.NiceDiff 881 881 } 882 882 883 883 func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 884 - params.Active = "overview" 885 - return p.executeRepo("repo/compare/new", w, params) 884 + return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 886 885 } 887 886 888 887 func (p *Pages) Static() http.Handler {
+157
appview/pages/templates/repo/compare.html
··· 1 + {{ define "title" }}new comparison{{ end }} 2 + 3 + {{ define "repoContent" }} 4 + <section> 5 + <h2 class="font-bold text-sm mb-4 uppercase dark:text-white"> 6 + Compare changes 7 + </h2> 8 + <p>Choose any two refs to compare.</p> 9 + 10 + <form id="compare-form"> 11 + <div class="flex items-center gap-2 py-4"> 12 + <div> 13 + base: 14 + 15 + <select 16 + name="base" 17 + id="base-select" 18 + class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 19 + onchange="triggerCompare()" 20 + > 21 + <optgroup 22 + label="branches ({{ len .Branches }})" 23 + class="bold text-sm" 24 + > 25 + {{ range .Branches }} 26 + <option 27 + value="{{ .Reference.Name }}" 28 + class="py-1" 29 + {{ if .IsDefault }} 30 + selected 31 + {{ end }} 32 + > 33 + {{ .Reference.Name }} 34 + </option> 35 + {{ end }} 36 + </optgroup> 37 + <optgroup 38 + label="tags ({{ len .Tags }})" 39 + class="bold text-sm" 40 + > 41 + {{ range .Tags }} 42 + <option 43 + value="{{ .Reference.Name }}" 44 + class="py-1" 45 + > 46 + {{ .Reference.Name }} 47 + </option> 48 + {{ else }} 49 + <option class="py-1" disabled> 50 + no tags found 51 + </option> 52 + {{ end }} 53 + </optgroup> 54 + </select> 55 + </div> 56 + 57 + {{ i "arrow-left" "w-4 h-4" }} 58 + 59 + 60 + <div> 61 + compare: 62 + 63 + <select 64 + name="head" 65 + id="head-select" 66 + class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 67 + onchange="triggerCompare()" 68 + > 69 + <option value="" selected disabled hidden> 70 + select a branch or tag 71 + </option> 72 + <optgroup 73 + label="branches ({{ len .Branches }})" 74 + class="bold text-sm" 75 + > 76 + {{ range .Branches }} 77 + <option 78 + value="{{ .Reference.Name }}" 79 + class="py-1" 80 + > 81 + {{ .Reference.Name }} 82 + </option> 83 + {{ end }} 84 + </optgroup> 85 + <optgroup 86 + label="tags ({{ len .Tags }})" 87 + class="bold text-sm" 88 + > 89 + {{ range .Tags }} 90 + <option 91 + value="{{ .Reference.Name }}" 92 + class="py-1" 93 + > 94 + {{ .Reference.Name }} 95 + </option> 96 + {{ else }} 97 + <option class="py-1" disabled> 98 + no tags found 99 + </option> 100 + {{ end }} 101 + </optgroup> 102 + </select> 103 + </div> 104 + </div> 105 + </form> 106 + </section> 107 + <section class="hidden"></section> 108 + 109 + <script> 110 + var templatedBase = `{{ .Base }}`; 111 + var templatedHead = `{{ .Head }}`; 112 + var selectedBase = ""; 113 + var selectedHead = ""; 114 + 115 + document.addEventListener('DOMContentLoaded', function() { 116 + if (templatedBase && templatedHead) { 117 + const baseSelect = document.getElementById('base-select'); 118 + const headSelect = document.getElementById('head-select'); 119 + 120 + // select the option that matches templated values 121 + for(let i = 0; i < baseSelect.options.length; i++) { 122 + if(baseSelect.options[i].value === templatedBase) { 123 + baseSelect.selectedIndex = i; 124 + break; 125 + } 126 + } 127 + 128 + for(let i = 0; i < headSelect.options.length; i++) { 129 + if(headSelect.options[i].value === templatedHead) { 130 + headSelect.selectedIndex = i; 131 + break; 132 + } 133 + } 134 + 135 + triggerCompare(); 136 + } 137 + }); 138 + 139 + function triggerCompare() { 140 + // if user has selected values, use those 141 + selectedBase = document.getElementById('base-select').value; 142 + selectedHead = document.getElementById('head-select').value; 143 + 144 + const baseToUse = templatedBase && !selectedBase ? templatedBase : selectedBase; 145 + const headToUse = templatedHead && !selectedHead ? templatedHead : selectedHead; 146 + 147 + if (baseToUse && headToUse) { 148 + const url = `/{{ .RepoInfo.FullName }}/compare/diff/${baseToUse}/${headToUse}`; 149 + htmx.ajax('GET', url, { target: '#compare-diff' }); 150 + } 151 + } 152 + </script> 153 + {{ end }} 154 + 155 + {{ define "repoAfter" }} 156 + <div id="compare-diff"></div> 157 + {{ end }}
-74
appview/pages/templates/repo/compare/new.html
··· 1 - {{ define "title" }}new comparison{{ end }} 2 - 3 - {{ define "repoContent" }} 4 - <h2 class="font-bold text-sm mb-4 uppercase dark:text-white"> 5 - Compare changes 6 - </h2> 7 - <p>Choose any two refs to compare.</p> 8 - 9 - <div class="flex items-center gap-2 py-4"> 10 - <div> 11 - base: 12 - <select 13 - class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 14 - > 15 - <optgroup 16 - label="branches ({{ len .Branches }})" 17 - class="bold text-sm" 18 - > 19 - {{ range .Branches }} 20 - <option 21 - value="{{ .Reference.Name }}" 22 - class="py-1" 23 - {{ if .IsDefault }} 24 - selected 25 - {{ end }} 26 - > 27 - {{ .Reference.Name }} 28 - </option> 29 - {{ end }} 30 - </optgroup> 31 - <optgroup label="tags ({{ len .Tags }})" class="bold text-sm"> 32 - {{ range .Tags }} 33 - <option value="{{ .Reference.Name }}" class="py-1"> 34 - {{ .Reference.Name }} 35 - </option> 36 - {{ else }} 37 - <option class="py-1" disabled>no tags found</option> 38 - {{ end }} 39 - </optgroup> 40 - </select> 41 - </div> 42 - {{ i "arrow-left" "w-4 h-4" }} 43 - <div> 44 - compare: 45 - <select 46 - class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 47 - > 48 - <optgroup 49 - label="branches ({{ len .Branches }})" 50 - class="bold text-sm" 51 - > 52 - {{ range .Branches }} 53 - <option value="{{ .Reference.Name }}" class="py-1"> 54 - {{ .Reference.Name }} 55 - </option> 56 - {{ end }} 57 - </optgroup> 58 - <optgroup label="tags ({{ len .Tags }})" class="bold text-sm"> 59 - {{ range .Tags }} 60 - <option value="{{ .Reference.Name }}" class="py-1"> 61 - {{ .Reference.Name }} 62 - </option> 63 - {{ else }} 64 - <option class="py-1" disabled>no tags found</option> 65 - {{ end }} 66 - </optgroup> 67 - </select> 68 - </div> 69 - </div> 70 - {{ end }} 71 - 72 - {{ define "repoAfter" }} 73 - <div id="compare-diff"></div> 74 - {{ end }}
+28 -11
appview/state/repo.go
··· 25 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 26 26 "tangled.sh/tangled.sh/core/appview/pagination" 27 27 "tangled.sh/tangled.sh/core/knotclient" 28 + "tangled.sh/tangled.sh/core/patchutil" 28 29 "tangled.sh/tangled.sh/core/types" 29 30 30 31 "github.com/bluesky-social/indigo/atproto/data" ··· 185 186 return nil, err 186 187 } 187 188 188 - if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 189 + if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 189 190 return branch.Name == f.Ref 190 191 }) { 191 192 forkInfo.Status = types.MissingBranch ··· 556 557 return 557 558 } 558 559 559 - slices.SortFunc(result.Branches, func(a, b types.Branch) int { 560 + slices.SortFunc(result.Branches, func(a, b types.Branch) int { 560 561 if a.IsDefault { 561 562 return -1 562 563 } ··· 2063 2064 return 2064 2065 } 2065 2066 2067 + // if user is navigating to one of 2068 + // /compare/{base}/{head} 2069 + // /compare/{base}...{head} 2070 + base := chi.URLParam(r, "base") 2071 + head := chi.URLParam(r, "head") 2072 + if base == "" && head == "" { 2073 + rest := chi.URLParam(r, "*") // master...feature/xyz 2074 + parts := strings.SplitN(rest, "...", 2) 2075 + if len(parts) == 2 { 2076 + base = parts[0] 2077 + head = parts[1] 2078 + } 2079 + } 2080 + 2066 2081 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 2067 2082 if err != nil { 2068 2083 log.Printf("failed to create unsigned client for %s", f.Knot) ··· 2101 2116 Forks: forks, 2102 2117 Branches: branches.Branches, 2103 2118 Tags: tags.Tags, 2119 + Base: base, 2120 + Head: head, 2104 2121 }) 2105 2122 } 2106 2123 2107 - func (s *State) RepoCompareDiff(w http.ResponseWriter, r *http.Request) { 2124 + func (s *State) RepoCompareDiffFragment(w http.ResponseWriter, r *http.Request) { 2108 2125 f, err := s.fullyResolvedRepo(r) 2109 2126 if err != nil { 2110 2127 log.Println("failed to get repo and knot", err) ··· 2112 2129 } 2113 2130 user := s.oauth.GetUser(r) 2114 2131 2115 - rest := chi.URLParam(r, "*") // master...feature/xyz 2116 - parts := strings.SplitN(rest, "...", 2) 2117 - if len(parts) != 2 { 2132 + base := chi.URLParam(r, "base") 2133 + head := chi.URLParam(r, "head") 2134 + 2135 + if base == "" || head == "" { 2118 2136 s.pages.Notice(w, "compare-error", "Invalid ref format.") 2119 2137 return 2120 2138 } 2121 2139 2122 - ref1 := parts[0] 2123 - ref2 := parts[1] 2124 - 2125 2140 us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 2126 2141 if err != nil { 2127 2142 s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") ··· 2129 2144 return 2130 2145 } 2131 2146 2132 - formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, ref1, ref2) 2147 + formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 2133 2148 if err != nil { 2134 2149 s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2135 2150 log.Println("failed to compare", err) 2136 2151 return 2137 2152 } 2153 + diff := patchutil.AsNiceDiff(formatPatch.Patch, base) 2138 2154 2155 + w.Header().Add("Hx-Push-Url", fmt.Sprintf("/%s/compare/%s...%s", f.OwnerSlashRepo(), base, head)) 2139 2156 s.pages.RepoCompareDiff(w, pages.RepoCompareDiffParams{ 2140 2157 LoggedInUser: user, 2141 2158 RepoInfo: f.RepoInfo(s, user), 2142 - FormatPatch: *formatPatch, 2159 + Diff: diff, 2143 2160 }) 2144 2161 }
+3 -1
appview/state/router.go
··· 126 126 // for example: 127 127 // /compare/master...some/feature 128 128 // /compare/master...example.com:another/feature <- this is a fork 129 - r.Get("/*", s.RepoCompareDiff) 129 + r.Get("/{base}/{head}", s.RepoCompare) 130 + r.Get("/*", s.RepoCompare) 131 + r.Get("/diff/{base}/{head}", s.RepoCompareDiffFragment) 130 132 }) 131 133 132 134 r.Route("/pulls", func(r chi.Router) {