forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview: pages: initial htmxing of compare page

Changed files
+193 -92
appview
pages
templates
repo
state
+5 -6
appview/pages/pages.go
···
Forks []db.Repo
Branches []types.Branch
Tags []*types.TagReference
Active string
}
func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
params.Active = "overview"
-
return p.executeRepo("repo/compare/new", w, params)
}
type RepoCompareDiffParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
FormatPatch types.RepoFormatPatchResponse
-
-
Active string
}
func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
-
params.Active = "overview"
-
return p.executeRepo("repo/compare/new", w, params)
}
func (p *Pages) Static() http.Handler {
···
Forks []db.Repo
Branches []types.Branch
Tags []*types.TagReference
+
Base string
+
Head string
Active string
}
func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
params.Active = "overview"
+
return p.executeRepo("repo/compare", w, params)
}
type RepoCompareDiffParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
+
Diff types.NiceDiff
}
func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
+
return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff})
}
func (p *Pages) Static() http.Handler {
+157
appview/pages/templates/repo/compare.html
···
···
+
{{ define "title" }}new comparison{{ end }}
+
+
{{ define "repoContent" }}
+
<section>
+
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">
+
Compare changes
+
</h2>
+
<p>Choose any two refs to compare.</p>
+
+
<form id="compare-form">
+
<div class="flex items-center gap-2 py-4">
+
<div>
+
base:
+
+
<select
+
name="base"
+
id="base-select"
+
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
+
onchange="triggerCompare()"
+
>
+
<optgroup
+
label="branches ({{ len .Branches }})"
+
class="bold text-sm"
+
>
+
{{ range .Branches }}
+
<option
+
value="{{ .Reference.Name }}"
+
class="py-1"
+
{{ if .IsDefault }}
+
selected
+
{{ end }}
+
>
+
{{ .Reference.Name }}
+
</option>
+
{{ end }}
+
</optgroup>
+
<optgroup
+
label="tags ({{ len .Tags }})"
+
class="bold text-sm"
+
>
+
{{ range .Tags }}
+
<option
+
value="{{ .Reference.Name }}"
+
class="py-1"
+
>
+
{{ .Reference.Name }}
+
</option>
+
{{ else }}
+
<option class="py-1" disabled>
+
no tags found
+
</option>
+
{{ end }}
+
</optgroup>
+
</select>
+
</div>
+
+
{{ i "arrow-left" "w-4 h-4" }}
+
+
+
<div>
+
compare:
+
+
<select
+
name="head"
+
id="head-select"
+
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
+
onchange="triggerCompare()"
+
>
+
<option value="" selected disabled hidden>
+
select a branch or tag
+
</option>
+
<optgroup
+
label="branches ({{ len .Branches }})"
+
class="bold text-sm"
+
>
+
{{ range .Branches }}
+
<option
+
value="{{ .Reference.Name }}"
+
class="py-1"
+
>
+
{{ .Reference.Name }}
+
</option>
+
{{ end }}
+
</optgroup>
+
<optgroup
+
label="tags ({{ len .Tags }})"
+
class="bold text-sm"
+
>
+
{{ range .Tags }}
+
<option
+
value="{{ .Reference.Name }}"
+
class="py-1"
+
>
+
{{ .Reference.Name }}
+
</option>
+
{{ else }}
+
<option class="py-1" disabled>
+
no tags found
+
</option>
+
{{ end }}
+
</optgroup>
+
</select>
+
</div>
+
</div>
+
</form>
+
</section>
+
<section class="hidden"></section>
+
+
<script>
+
var templatedBase = `{{ .Base }}`;
+
var templatedHead = `{{ .Head }}`;
+
var selectedBase = "";
+
var selectedHead = "";
+
+
document.addEventListener('DOMContentLoaded', function() {
+
if (templatedBase && templatedHead) {
+
const baseSelect = document.getElementById('base-select');
+
const headSelect = document.getElementById('head-select');
+
+
// select the option that matches templated values
+
for(let i = 0; i < baseSelect.options.length; i++) {
+
if(baseSelect.options[i].value === templatedBase) {
+
baseSelect.selectedIndex = i;
+
break;
+
}
+
}
+
+
for(let i = 0; i < headSelect.options.length; i++) {
+
if(headSelect.options[i].value === templatedHead) {
+
headSelect.selectedIndex = i;
+
break;
+
}
+
}
+
+
triggerCompare();
+
}
+
});
+
+
function triggerCompare() {
+
// if user has selected values, use those
+
selectedBase = document.getElementById('base-select').value;
+
selectedHead = document.getElementById('head-select').value;
+
+
const baseToUse = templatedBase && !selectedBase ? templatedBase : selectedBase;
+
const headToUse = templatedHead && !selectedHead ? templatedHead : selectedHead;
+
+
if (baseToUse && headToUse) {
+
const url = `/{{ .RepoInfo.FullName }}/compare/diff/${baseToUse}/${headToUse}`;
+
htmx.ajax('GET', url, { target: '#compare-diff' });
+
}
+
}
+
</script>
+
{{ end }}
+
+
{{ define "repoAfter" }}
+
<div id="compare-diff"></div>
+
{{ end }}
-74
appview/pages/templates/repo/compare/new.html
···
-
{{ define "title" }}new comparison{{ end }}
-
-
{{ define "repoContent" }}
-
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">
-
Compare changes
-
</h2>
-
<p>Choose any two refs to compare.</p>
-
-
<div class="flex items-center gap-2 py-4">
-
<div>
-
base:
-
<select
-
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
-
>
-
<optgroup
-
label="branches ({{ len .Branches }})"
-
class="bold text-sm"
-
>
-
{{ range .Branches }}
-
<option
-
value="{{ .Reference.Name }}"
-
class="py-1"
-
{{ if .IsDefault }}
-
selected
-
{{ end }}
-
>
-
{{ .Reference.Name }}
-
</option>
-
{{ end }}
-
</optgroup>
-
<optgroup label="tags ({{ len .Tags }})" class="bold text-sm">
-
{{ range .Tags }}
-
<option value="{{ .Reference.Name }}" class="py-1">
-
{{ .Reference.Name }}
-
</option>
-
{{ else }}
-
<option class="py-1" disabled>no tags found</option>
-
{{ end }}
-
</optgroup>
-
</select>
-
</div>
-
{{ i "arrow-left" "w-4 h-4" }}
-
<div>
-
compare:
-
<select
-
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
-
>
-
<optgroup
-
label="branches ({{ len .Branches }})"
-
class="bold text-sm"
-
>
-
{{ range .Branches }}
-
<option value="{{ .Reference.Name }}" class="py-1">
-
{{ .Reference.Name }}
-
</option>
-
{{ end }}
-
</optgroup>
-
<optgroup label="tags ({{ len .Tags }})" class="bold text-sm">
-
{{ range .Tags }}
-
<option value="{{ .Reference.Name }}" class="py-1">
-
{{ .Reference.Name }}
-
</option>
-
{{ else }}
-
<option class="py-1" disabled>no tags found</option>
-
{{ end }}
-
</optgroup>
-
</select>
-
</div>
-
</div>
-
{{ end }}
-
-
{{ define "repoAfter" }}
-
<div id="compare-diff"></div>
-
{{ end }}
···
+28 -11
appview/state/repo.go
···
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
"tangled.sh/tangled.sh/core/appview/pagination"
"tangled.sh/tangled.sh/core/knotclient"
"tangled.sh/tangled.sh/core/types"
"github.com/bluesky-social/indigo/atproto/data"
···
return nil, err
}
-
if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
return branch.Name == f.Ref
}) {
forkInfo.Status = types.MissingBranch
···
return
}
-
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
if a.IsDefault {
return -1
}
···
return
}
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
···
Forks: forks,
Branches: branches.Branches,
Tags: tags.Tags,
})
}
-
func (s *State) RepoCompareDiff(w http.ResponseWriter, r *http.Request) {
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
···
}
user := s.oauth.GetUser(r)
-
rest := chi.URLParam(r, "*") // master...feature/xyz
-
parts := strings.SplitN(rest, "...", 2)
-
if len(parts) != 2 {
s.pages.Notice(w, "compare-error", "Invalid ref format.")
return
}
-
ref1 := parts[0]
-
ref2 := parts[1]
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
···
return
}
-
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, ref1, ref2)
if err != nil {
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to compare", err)
return
}
s.pages.RepoCompareDiff(w, pages.RepoCompareDiffParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(s, user),
-
FormatPatch: *formatPatch,
})
}
···
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
"tangled.sh/tangled.sh/core/appview/pagination"
"tangled.sh/tangled.sh/core/knotclient"
+
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/types"
"github.com/bluesky-social/indigo/atproto/data"
···
return nil, err
}
+
if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
return branch.Name == f.Ref
}) {
forkInfo.Status = types.MissingBranch
···
return
}
+
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
if a.IsDefault {
return -1
}
···
return
}
+
// if user is navigating to one of
+
// /compare/{base}/{head}
+
// /compare/{base}...{head}
+
base := chi.URLParam(r, "base")
+
head := chi.URLParam(r, "head")
+
if base == "" && head == "" {
+
rest := chi.URLParam(r, "*") // master...feature/xyz
+
parts := strings.SplitN(rest, "...", 2)
+
if len(parts) == 2 {
+
base = parts[0]
+
head = parts[1]
+
}
+
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
···
Forks: forks,
Branches: branches.Branches,
Tags: tags.Tags,
+
Base: base,
+
Head: head,
})
}
+
func (s *State) RepoCompareDiffFragment(w http.ResponseWriter, r *http.Request) {
f, err := s.fullyResolvedRepo(r)
if err != nil {
log.Println("failed to get repo and knot", err)
···
}
user := s.oauth.GetUser(r)
+
base := chi.URLParam(r, "base")
+
head := chi.URLParam(r, "head")
+
+
if base == "" || head == "" {
s.pages.Notice(w, "compare-error", "Invalid ref format.")
return
}
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
···
return
}
+
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head)
if err != nil {
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to compare", err)
return
}
+
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
+
w.Header().Add("Hx-Push-Url", fmt.Sprintf("/%s/compare/%s...%s", f.OwnerSlashRepo(), base, head))
s.pages.RepoCompareDiff(w, pages.RepoCompareDiffParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(s, user),
+
Diff: diff,
})
}
+3 -1
appview/state/router.go
···
// for example:
// /compare/master...some/feature
// /compare/master...example.com:another/feature <- this is a fork
-
r.Get("/*", s.RepoCompareDiff)
})
r.Route("/pulls", func(r chi.Router) {
···
// for example:
// /compare/master...some/feature
// /compare/master...example.com:another/feature <- this is a fork
+
r.Get("/{base}/{head}", s.RepoCompare)
+
r.Get("/*", s.RepoCompare)
+
r.Get("/diff/{base}/{head}", s.RepoCompareDiffFragment)
})
r.Route("/pulls", func(r chi.Router) {