Extract pagination UI into a shared template for reuse across pages. Refactored issues listing to use it with no behavior change
+2
appview/pages/pages.go
+2
appview/pages/pages.go
+95
appview/pages/templates/fragments/pagination.html
+95
appview/pages/templates/fragments/pagination.html
···
1
+
{{ define "fragments/pagination" }}
2
+
{{/* Params: Page (pagination.Page), TotalCount (int), BasePath (string), QueryParams (string) */}}
3
+
{{ $page := .Page }}
4
+
{{ $totalCount := .TotalCount }}
5
+
{{ $basePath := .BasePath }}
6
+
{{ $queryParams := .QueryParams }}
7
+
8
+
{{ $prev := $page.Previous.Offset }}
9
+
{{ $next := $page.Next.Offset }}
10
+
{{ $lastPage := sub $totalCount (mod $totalCount $page.Limit) }}
11
+
12
+
<div class="flex justify-center items-center mt-4 gap-2">
13
+
<a
14
+
class="
15
+
btn flex items-center gap-2 no-underline hover:no-underline
16
+
dark:text-white dark:hover:bg-gray-700
17
+
{{ if le $page.Offset 0 }}
18
+
cursor-not-allowed opacity-50
19
+
{{ end }}
20
+
"
21
+
{{ if gt $page.Offset 0 }}
22
+
hx-boost="true"
23
+
href="{{ $basePath }}?{{ $queryParams }}&offset={{ $prev }}&limit={{ $page.Limit }}"
24
+
{{ end }}
25
+
>
26
+
{{ i "chevron-left" "w-4 h-4" }}
27
+
previous
28
+
</a>
29
+
30
+
{{ if gt $page.Offset 0 }}
31
+
<a
32
+
hx-boost="true"
33
+
href="{{ $basePath }}?{{ $queryParams }}&offset=0&limit={{ $page.Limit }}"
34
+
>
35
+
1
36
+
</a>
37
+
{{ end }}
38
+
39
+
{{ if gt $prev $page.Limit }}
40
+
<span>...</span>
41
+
{{ end }}
42
+
43
+
{{ if gt $prev 0 }}
44
+
<a
45
+
hx-boost="true"
46
+
href="{{ $basePath }}?{{ $queryParams }}&offset={{ $prev }}&limit={{ $page.Limit }}"
47
+
>
48
+
{{ add (div $prev $page.Limit) 1 }}
49
+
</a>
50
+
{{ end }}
51
+
52
+
<span class="font-bold">
53
+
{{ add (div $page.Offset $page.Limit) 1 }}
54
+
</span>
55
+
56
+
{{ if lt $next $lastPage }}
57
+
<a
58
+
hx-boost="true"
59
+
href="{{ $basePath }}?{{ $queryParams }}&offset={{ $next }}&limit={{ $page.Limit }}"
60
+
>
61
+
{{ add (div $next $page.Limit) 1 }}
62
+
</a>
63
+
{{ end }}
64
+
65
+
{{ if lt $next (sub $totalCount (mul 2 $page.Limit)) }}
66
+
<span>...</span>
67
+
{{ end }}
68
+
69
+
{{ if lt $page.Offset $lastPage }}
70
+
<a
71
+
hx-boost="true"
72
+
href="{{ $basePath }}?{{ $queryParams }}&offset={{ $lastPage }}&limit={{ $page.Limit }}"
73
+
>
74
+
{{ add (div $lastPage $page.Limit) 1 }}
75
+
</a>
76
+
{{ end }}
77
+
78
+
<a
79
+
class="
80
+
btn flex items-center gap-2 no-underline hover:no-underline
81
+
dark:text-white dark:hover:bg-gray-700
82
+
{{ if lt $next $totalCount | not }}
83
+
cursor-not-allowed opacity-50
84
+
{{ end }}
85
+
"
86
+
{{ if lt $next $totalCount }}
87
+
hx-boost="true"
88
+
href="{{ $basePath }}?{{ $queryParams }}&offset={{ $next }}&limit={{ $page.Limit }}"
89
+
{{ end }}
90
+
>
91
+
next
92
+
{{ i "chevron-right" "w-4 h-4" }}
93
+
</a>
94
+
</div>
95
+
{{ end }}
+10
-103
appview/pages/templates/repo/issues/issues.html
+10
-103
appview/pages/templates/repo/issues/issues.html
···
71
71
<div class="mt-2">
72
72
{{ template "repo/issues/fragments/issueListing" (dict "Issues" .Issues "RepoPrefix" .RepoInfo.FullName "LabelDefs" .LabelDefs) }}
73
73
</div>
74
-
{{if gt .IssueCount .Page.Limit }}
75
-
{{ block "pagination" . }} {{ end }}
76
-
{{ end }}
77
-
{{ end }}
78
-
79
-
{{ define "pagination" }}
80
-
<div class="flex justify-center items-center mt-4 gap-2">
81
-
{{ $currentState := "closed" }}
82
-
{{ if .FilteringByOpen }}
83
-
{{ $currentState = "open" }}
84
-
{{ end }}
85
-
86
-
{{ $prev := .Page.Previous.Offset }}
87
-
{{ $next := .Page.Next.Offset }}
88
-
{{ $lastPage := sub .IssueCount (mod .IssueCount .Page.Limit) }}
89
-
90
-
<a
91
-
class="
92
-
btn flex items-center gap-2 no-underline hover:no-underline
93
-
dark:text-white dark:hover:bg-gray-700
94
-
{{ if le .Page.Offset 0 }}
95
-
cursor-not-allowed opacity-50
96
-
{{ end }}
97
-
"
98
-
{{ if gt .Page.Offset 0 }}
99
-
hx-boost="true"
100
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev }}&limit={{ .Page.Limit }}"
74
+
{{if gt .IssueCount .Page.Limit }}
75
+
{{ $state := "closed" }}
76
+
{{ if .FilteringByOpen }}
77
+
{{ $state = "open" }}
101
78
{{ end }}
102
-
>
103
-
{{ i "chevron-left" "w-4 h-4" }}
104
-
previous
105
-
</a>
106
-
107
-
<!-- dont show first page if current page is first page -->
108
-
{{ if gt .Page.Offset 0 }}
109
-
<a
110
-
hx-boost="true"
111
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset=0&limit={{ .Page.Limit }}"
112
-
>
113
-
1
114
-
</a>
115
-
{{ end }}
116
-
117
-
<!-- if previous page is not first or second page (prev > limit) -->
118
-
{{ if gt $prev .Page.Limit }}
119
-
<span>...</span>
79
+
{{ template "fragments/pagination" (dict
80
+
"Page" .Page
81
+
"TotalCount" .IssueCount
82
+
"BasePath" (printf "/%s/issues" .RepoInfo.FullName)
83
+
"QueryParams" (printf "state=%s&q=%s" $state .FilterQuery)
84
+
) }}
120
85
{{ end }}
121
-
122
-
<!-- if previous page is not the first page -->
123
-
{{ if gt $prev 0 }}
124
-
<a
125
-
hx-boost="true"
126
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev }}&limit={{ .Page.Limit }}"
127
-
>
128
-
{{ add (div $prev .Page.Limit) 1 }}
129
-
</a>
130
-
{{ end }}
131
-
132
-
<!-- current page. this is always visible -->
133
-
<span class="font-bold">
134
-
{{ add (div .Page.Offset .Page.Limit) 1 }}
135
-
</span>
136
-
137
-
<!-- if next page is not last page -->
138
-
{{ if lt $next $lastPage }}
139
-
<a
140
-
hx-boost="true"
141
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next }}&limit={{ .Page.Limit }}"
142
-
>
143
-
{{ add (div $next .Page.Limit) 1 }}
144
-
</a>
145
-
{{ end }}
146
-
147
-
<!-- if next page is not second last or last page (next < issues - 2 * limit) -->
148
-
{{ if lt ($next) (sub .IssueCount (mul (2) .Page.Limit)) }}
149
-
<span>...</span>
150
-
{{ end }}
151
-
152
-
<!-- if its not the last page -->
153
-
{{ if lt .Page.Offset $lastPage }}
154
-
<a
155
-
hx-boost="true"
156
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $lastPage }}&limit={{ .Page.Limit }}"
157
-
>
158
-
{{ add (div $lastPage .Page.Limit) 1 }}
159
-
</a>
160
-
{{ end }}
161
-
162
-
<a
163
-
class="
164
-
btn flex items-center gap-2 no-underline hover:no-underline
165
-
dark:text-white dark:hover:bg-gray-700
166
-
{{ if ne (len .Issues) .Page.Limit }}
167
-
cursor-not-allowed opacity-50
168
-
{{ end }}
169
-
"
170
-
{{ if eq (len .Issues) .Page.Limit }}
171
-
hx-boost="true"
172
-
href="/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next }}&limit={{ .Page.Limit }}"
173
-
{{ end }}
174
-
>
175
-
next
176
-
{{ i "chevron-right" "w-4 h-4" }}
177
-
</a>
178
-
</div>
179
86
{{ end }}
+8
appview/pages/templates/repo/pulls/pulls.html
+8
appview/pages/templates/repo/pulls/pulls.html
···
170
170
</div>
171
171
{{ end }}
172
172
</div>
173
+
{{if gt .PullCount .Page.Limit }}
174
+
{{ template "fragments/pagination" (dict
175
+
"Page" .Page
176
+
"TotalCount" .PullCount
177
+
"BasePath" (printf "/%s/pulls" .RepoInfo.FullName)
178
+
"QueryParams" (printf "state=%s&q=%s" .FilteringBy.String .FilterQuery)
179
+
) }}
180
+
{{ end }}
173
181
{{ end }}
174
182
175
183
{{ define "stackedPullList" }}
+17
-1
appview/pulls/pulls.go
+17
-1
appview/pulls/pulls.go
···
26
26
"tangled.org/core/appview/pages"
27
27
"tangled.org/core/appview/pages/markup"
28
28
"tangled.org/core/appview/pages/repoinfo"
29
+
"tangled.org/core/appview/pagination"
29
30
"tangled.org/core/appview/reporesolver"
30
31
"tangled.org/core/appview/validator"
31
32
"tangled.org/core/appview/xrpcclient"
···
563
564
state = models.PullMerged
564
565
}
565
566
567
+
page := pagination.FromContext(r.Context())
568
+
566
569
f, err := s.repoResolver.Resolve(r)
567
570
if err != nil {
568
571
log.Println("failed to get repo and knot", err)
569
572
return
570
573
}
571
574
575
+
var totalPulls int
576
+
switch state {
577
+
case models.PullOpen:
578
+
totalPulls = f.RepoStats.PullCount.Open
579
+
case models.PullMerged:
580
+
totalPulls = f.RepoStats.PullCount.Merged
581
+
case models.PullClosed:
582
+
totalPulls = f.RepoStats.PullCount.Closed
583
+
}
584
+
572
585
keyword := params.Get("q")
573
586
574
587
var ids []int64
···
576
589
Keyword: keyword,
577
590
RepoAt: f.RepoAt().String(),
578
591
State: state,
579
-
// Page: page,
592
+
Page: page,
580
593
}
581
594
l.Debug("searching with", "searchOpts", searchOpts)
582
595
if keyword != "" {
···
586
599
return
587
600
}
588
601
ids = res.Hits
602
+
totalPulls = int(res.Total)
589
603
l.Debug("searched pulls with indexer", "count", len(ids))
590
604
} else {
591
605
ids, err = db.GetPullIDs(s.db, searchOpts)
···
688
702
FilterQuery: keyword,
689
703
Stacks: stacks,
690
704
Pipelines: m,
705
+
Page: page,
706
+
PullCount: totalPulls,
691
707
})
692
708
}
693
709
+1
-1
appview/pulls/router.go
+1
-1
appview/pulls/router.go
···
9
9
10
10
func (s *Pulls) Router(mw *middleware.Middleware) http.Handler {
11
11
r := chi.NewRouter()
12
-
r.Get("/", s.RepoPulls)
12
+
r.With(middleware.Paginate).Get("/", s.RepoPulls)
13
13
r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) {
14
14
r.Get("/", s.NewPull)
15
15
r.Get("/patch-upload", s.PatchUploadFragment)
History
1 round
3 comments
moshyfawn.dev
submitted
#0
2 commits
expand
collapse
appview: add reusable pagination template
Extract pagination UI into a shared template for reuse across pages. Refactored issues listing to use it with no behavior change
appview/pulls: add pagination to pull requests listing
Add offset-based pagination to the pulls page, matching the issues behavior
expand 3 comments
Thank you for the contribution! This generally seems good, but PR list pagination is inverted due to db.GetPageIDs() bug. So I opened a new PR with the fix amended. I'll leave you as the author of the commit.
closed without merging
Can't update the desc: forgot it only extracts the first commit message.
This PR adds offset-based pagination to the pulls page using the refactored issues pagination template. This is relevant for both https://tangled.org/tangled.org/core/pulls/951 and the fact that right now going to pulls to look through some merged PRs pulls the entire set of merged PRs data which is a DB operation that's on a longer running side.