+35
-20
appview/db/issues.go
+35
-20
appview/db/issues.go
···
5
5
"time"
6
6
7
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"tangled.sh/tangled.sh/core/appview/pagination"
8
9
)
9
10
10
11
type Issue struct {
···
102
103
return ownerDid, err
103
104
}
104
105
105
-
func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) {
106
+
func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
106
107
var issues []Issue
107
108
openValue := 0
108
109
if isOpen {
···
110
111
}
111
112
112
113
rows, err := e.Query(
113
-
`select
114
-
i.owner_did,
115
-
i.issue_id,
116
-
i.created,
117
-
i.title,
118
-
i.body,
119
-
i.open,
120
-
count(c.id)
121
-
from
122
-
issues i
123
-
left join
124
-
comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
125
-
where
126
-
i.repo_at = ? and i.open = ?
127
-
group by
128
-
i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open
129
-
order by
130
-
i.created desc`,
131
-
repoAt, openValue)
114
+
`
115
+
with numbered_issue as (
116
+
select
117
+
i.owner_did,
118
+
i.issue_id,
119
+
i.created,
120
+
i.title,
121
+
i.body,
122
+
i.open,
123
+
count(c.id) as comment_count,
124
+
row_number() over (order by i.created desc) as row_num
125
+
from
126
+
issues i
127
+
left join
128
+
comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
129
+
where
130
+
i.repo_at = ? and i.open = ?
131
+
group by
132
+
i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open
133
+
)
134
+
select
135
+
owner_did,
136
+
issue_id,
137
+
created,
138
+
title,
139
+
body,
140
+
open,
141
+
comment_count
142
+
from
143
+
numbered_issue
144
+
where
145
+
row_num between ? and ?`,
146
+
repoAt, openValue, page.Offset+1, page.Offset+page.Limit)
132
147
if err != nil {
133
148
return nil, err
134
149
}
+62
appview/filetree/filetree.go
+62
appview/filetree/filetree.go
···
1
+
package filetree
2
+
3
+
import (
4
+
"path/filepath"
5
+
"sort"
6
+
"strings"
7
+
)
8
+
9
+
type FileTreeNode struct {
10
+
Name string
11
+
Path string
12
+
IsDirectory bool
13
+
Children map[string]*FileTreeNode
14
+
}
15
+
16
+
// NewNode creates a new node
17
+
func newNode(name, path string, isDir bool) *FileTreeNode {
18
+
return &FileTreeNode{
19
+
Name: name,
20
+
Path: path,
21
+
IsDirectory: isDir,
22
+
Children: make(map[string]*FileTreeNode),
23
+
}
24
+
}
25
+
26
+
func FileTree(files []string) *FileTreeNode {
27
+
rootNode := newNode("", "", true)
28
+
29
+
sort.Strings(files)
30
+
31
+
for _, file := range files {
32
+
if file == "" {
33
+
continue
34
+
}
35
+
36
+
parts := strings.Split(filepath.Clean(file), "/")
37
+
if len(parts) == 0 {
38
+
continue
39
+
}
40
+
41
+
currentNode := rootNode
42
+
currentPath := ""
43
+
44
+
for i, part := range parts {
45
+
if currentPath == "" {
46
+
currentPath = part
47
+
} else {
48
+
currentPath = filepath.Join(currentPath, part)
49
+
}
50
+
51
+
isDir := i < len(parts)-1
52
+
53
+
if _, exists := currentNode.Children[part]; !exists {
54
+
currentNode.Children[part] = newNode(part, currentPath, isDir)
55
+
}
56
+
57
+
currentNode = currentNode.Children[part]
58
+
}
59
+
}
60
+
61
+
return rootNode
62
+
}
+32
appview/middleware/middleware.go
+32
appview/middleware/middleware.go
···
1
1
package middleware
2
2
3
3
import (
4
+
"context"
4
5
"log"
5
6
"net/http"
7
+
"strconv"
6
8
"time"
7
9
8
10
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
11
"github.com/bluesky-social/indigo/xrpc"
10
12
"tangled.sh/tangled.sh/core/appview"
11
13
"tangled.sh/tangled.sh/core/appview/auth"
14
+
"tangled.sh/tangled.sh/core/appview/pagination"
12
15
)
13
16
14
17
type Middleware func(http.Handler) http.Handler
···
92
95
})
93
96
}
94
97
}
98
+
99
+
func Paginate(next http.Handler) http.Handler {
100
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101
+
page := pagination.FirstPage()
102
+
103
+
offsetVal := r.URL.Query().Get("offset")
104
+
if offsetVal != "" {
105
+
offset, err := strconv.Atoi(offsetVal)
106
+
if err != nil {
107
+
log.Println("invalid offset")
108
+
} else {
109
+
page.Offset = offset
110
+
}
111
+
}
112
+
113
+
limitVal := r.URL.Query().Get("limit")
114
+
if limitVal != "" {
115
+
limit, err := strconv.Atoi(limitVal)
116
+
if err != nil {
117
+
log.Println("invalid limit")
118
+
} else {
119
+
page.Limit = limit
120
+
}
121
+
}
122
+
123
+
ctx := context.WithValue(r.Context(), "page", page)
124
+
next.ServeHTTP(w, r.WithContext(ctx))
125
+
})
126
+
}
+2
appview/pages/funcmap.go
+2
appview/pages/funcmap.go
···
13
13
"time"
14
14
15
15
"github.com/dustin/go-humanize"
16
+
"tangled.sh/tangled.sh/core/appview/filetree"
16
17
"tangled.sh/tangled.sh/core/appview/pages/markup"
17
18
)
18
19
···
174
175
return template.HTML(data)
175
176
},
176
177
"cssContentHash": CssContentHash,
178
+
"fileTree": filetree.FileTree,
177
179
}
178
180
}
179
181
+129
-37
appview/pages/pages.go
+129
-37
appview/pages/pages.go
···
11
11
"io/fs"
12
12
"log"
13
13
"net/http"
14
+
"os"
14
15
"path"
15
16
"path/filepath"
16
17
"slices"
···
19
20
"tangled.sh/tangled.sh/core/appview/auth"
20
21
"tangled.sh/tangled.sh/core/appview/db"
21
22
"tangled.sh/tangled.sh/core/appview/pages/markup"
23
+
"tangled.sh/tangled.sh/core/appview/pagination"
22
24
"tangled.sh/tangled.sh/core/appview/state/userutil"
23
25
"tangled.sh/tangled.sh/core/patchutil"
24
26
"tangled.sh/tangled.sh/core/types"
···
35
37
var Files embed.FS
36
38
37
39
type Pages struct {
38
-
t map[string]*template.Template
40
+
t map[string]*template.Template
41
+
dev bool
42
+
embedFS embed.FS
43
+
templateDir string // Path to templates on disk for dev mode
39
44
}
40
45
41
-
func NewPages() *Pages {
42
-
templates := make(map[string]*template.Template)
46
+
func NewPages(dev bool) *Pages {
47
+
p := &Pages{
48
+
t: make(map[string]*template.Template),
49
+
dev: dev,
50
+
embedFS: Files,
51
+
templateDir: "appview/pages",
52
+
}
43
53
54
+
// Initial load of all templates
55
+
p.loadAllTemplates()
56
+
57
+
return p
58
+
}
59
+
60
+
func (p *Pages) loadAllTemplates() {
61
+
templates := make(map[string]*template.Template)
44
62
var fragmentPaths []string
63
+
64
+
// Use embedded FS for initial loading
45
65
// First, collect all fragment paths
46
-
err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error {
66
+
err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
47
67
if err != nil {
48
68
return err
49
69
}
50
-
51
70
if d.IsDir() {
52
71
return nil
53
72
}
54
-
55
73
if !strings.HasSuffix(path, ".html") {
56
74
return nil
57
75
}
58
-
59
76
if !strings.Contains(path, "fragments/") {
60
77
return nil
61
78
}
62
-
63
79
name := strings.TrimPrefix(path, "templates/")
64
80
name = strings.TrimSuffix(name, ".html")
65
-
66
81
tmpl, err := template.New(name).
67
82
Funcs(funcMap()).
68
-
ParseFS(Files, path)
83
+
ParseFS(p.embedFS, path)
69
84
if err != nil {
70
85
log.Fatalf("setting up fragment: %v", err)
71
86
}
72
-
73
87
templates[name] = tmpl
74
88
fragmentPaths = append(fragmentPaths, path)
75
89
log.Printf("loaded fragment: %s", name)
···
80
94
}
81
95
82
96
// Then walk through and setup the rest of the templates
83
-
err = fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error {
97
+
err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
84
98
if err != nil {
85
99
return err
86
100
}
87
-
88
101
if d.IsDir() {
89
102
return nil
90
103
}
91
-
92
104
if !strings.HasSuffix(path, "html") {
93
105
return nil
94
106
}
95
-
96
107
// Skip fragments as they've already been loaded
97
108
if strings.Contains(path, "fragments/") {
98
109
return nil
99
110
}
100
-
101
111
// Skip layouts
102
112
if strings.Contains(path, "layouts/") {
103
113
return nil
104
114
}
105
-
106
115
name := strings.TrimPrefix(path, "templates/")
107
116
name = strings.TrimSuffix(name, ".html")
108
-
109
117
// Add the page template on top of the base
110
118
allPaths := []string{}
111
119
allPaths = append(allPaths, "templates/layouts/*.html")
···
113
121
allPaths = append(allPaths, path)
114
122
tmpl, err := template.New(name).
115
123
Funcs(funcMap()).
116
-
ParseFS(Files, allPaths...)
124
+
ParseFS(p.embedFS, allPaths...)
117
125
if err != nil {
118
126
return fmt.Errorf("setting up template: %w", err)
119
127
}
120
-
121
128
templates[name] = tmpl
122
129
log.Printf("loaded template: %s", name)
123
130
return nil
···
127
134
}
128
135
129
136
log.Printf("total templates loaded: %d", len(templates))
137
+
p.t = templates
138
+
}
130
139
131
-
return &Pages{
132
-
t: templates,
140
+
// loadTemplateFromDisk loads a template from the filesystem in dev mode
141
+
func (p *Pages) loadTemplateFromDisk(name string) error {
142
+
if !p.dev {
143
+
return nil
144
+
}
145
+
146
+
log.Printf("reloading template from disk: %s", name)
147
+
148
+
// Find all fragments first
149
+
var fragmentPaths []string
150
+
err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error {
151
+
if err != nil {
152
+
return err
153
+
}
154
+
if d.IsDir() {
155
+
return nil
156
+
}
157
+
if !strings.HasSuffix(path, ".html") {
158
+
return nil
159
+
}
160
+
if !strings.Contains(path, "fragments/") {
161
+
return nil
162
+
}
163
+
fragmentPaths = append(fragmentPaths, path)
164
+
return nil
165
+
})
166
+
if err != nil {
167
+
return fmt.Errorf("walking disk template dir for fragments: %w", err)
133
168
}
169
+
170
+
// Find the template path on disk
171
+
templatePath := filepath.Join(p.templateDir, "templates", name+".html")
172
+
if _, err := os.Stat(templatePath); os.IsNotExist(err) {
173
+
return fmt.Errorf("template not found on disk: %s", name)
174
+
}
175
+
176
+
// Create a new template
177
+
tmpl := template.New(name).Funcs(funcMap())
178
+
179
+
// Parse layouts
180
+
layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html")
181
+
layouts, err := filepath.Glob(layoutGlob)
182
+
if err != nil {
183
+
return fmt.Errorf("finding layout templates: %w", err)
184
+
}
185
+
186
+
// Create paths for parsing
187
+
allFiles := append(layouts, fragmentPaths...)
188
+
allFiles = append(allFiles, templatePath)
189
+
190
+
// Parse all templates
191
+
tmpl, err = tmpl.ParseFiles(allFiles...)
192
+
if err != nil {
193
+
return fmt.Errorf("parsing template files: %w", err)
194
+
}
195
+
196
+
// Update the template in the map
197
+
p.t[name] = tmpl
198
+
log.Printf("template reloaded from disk: %s", name)
199
+
return nil
134
200
}
135
201
136
-
type LoginParams struct {
202
+
func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error {
203
+
// In dev mode, reload the template from disk before executing
204
+
if p.dev {
205
+
if err := p.loadTemplateFromDisk(templateName); err != nil {
206
+
log.Printf("warning: failed to reload template %s from disk: %v", templateName, err)
207
+
// Continue with the existing template
208
+
}
209
+
}
210
+
211
+
tmpl, exists := p.t[templateName]
212
+
if !exists {
213
+
return fmt.Errorf("template not found: %s", templateName)
214
+
}
215
+
216
+
if base == "" {
217
+
return tmpl.Execute(w, params)
218
+
} else {
219
+
return tmpl.ExecuteTemplate(w, base, params)
220
+
}
137
221
}
138
222
139
223
func (p *Pages) execute(name string, w io.Writer, params any) error {
140
-
return p.t[name].ExecuteTemplate(w, "layouts/base", params)
224
+
return p.executeOrReload(name, w, "layouts/base", params)
141
225
}
142
226
143
227
func (p *Pages) executePlain(name string, w io.Writer, params any) error {
144
-
return p.t[name].Execute(w, params)
228
+
return p.executeOrReload(name, w, "", params)
145
229
}
146
230
147
231
func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
148
-
return p.t[name].ExecuteTemplate(w, "layouts/repobase", params)
232
+
return p.executeOrReload(name, w, "layouts/repobase", params)
233
+
}
234
+
235
+
type LoginParams struct {
149
236
}
150
237
151
238
func (p *Pages) Login(w io.Writer, params LoginParams) error {
···
419
506
}
420
507
421
508
type RepoCommitParams struct {
422
-
LoggedInUser *auth.User
423
-
RepoInfo RepoInfo
424
-
Active string
509
+
LoggedInUser *auth.User
510
+
RepoInfo RepoInfo
511
+
Active string
512
+
EmailToDidOrHandle map[string]string
513
+
425
514
types.RepoCommitResponse
426
-
EmailToDidOrHandle map[string]string
427
515
}
428
516
429
517
func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
···
564
652
}
565
653
566
654
type RepoIssuesParams struct {
567
-
LoggedInUser *auth.User
568
-
RepoInfo RepoInfo
569
-
Active string
570
-
Issues []db.Issue
571
-
DidHandleMap map[string]string
572
-
655
+
LoggedInUser *auth.User
656
+
RepoInfo RepoInfo
657
+
Active string
658
+
Issues []db.Issue
659
+
DidHandleMap map[string]string
660
+
Page pagination.Page
573
661
FilteringByOpen bool
574
662
}
575
663
···
698
786
DidHandleMap map[string]string
699
787
RepoInfo RepoInfo
700
788
Pull *db.Pull
701
-
Diff types.NiceDiff
789
+
Diff *types.NiceDiff
702
790
Round int
703
791
Submission *db.PullSubmission
704
792
}
···
794
882
}
795
883
796
884
func (p *Pages) Static() http.Handler {
885
+
if p.dev {
886
+
return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
887
+
}
888
+
797
889
sub, err := fs.Sub(Files, "static")
798
890
if err != nil {
799
891
log.Fatalf("no static dir found? that's crazy: %v", err)
+4
-17
appview/pages/templates/repo/fragments/diff.html
+4
-17
appview/pages/templates/repo/fragments/diff.html
···
3
3
{{ $diff := index . 1 }}
4
4
{{ $commit := $diff.Commit }}
5
5
{{ $stat := $diff.Stat }}
6
+
{{ $fileTree := fileTree $diff.ChangedFiles }}
6
7
{{ $diff := $diff.Diff }}
7
8
8
9
{{ $this := $commit.This }}
···
14
15
<strong class="text-sm uppercase dark:text-gray-200">Changed files</strong>
15
16
{{ block "statPill" $stat }} {{ end }}
16
17
</div>
17
-
<div class="overflow-x-auto">
18
-
{{ range $diff }}
19
-
<ul class="dark:text-gray-200">
20
-
{{ if .IsDelete }}
21
-
<li><a href="#file-{{ .Name.Old }}" class="dark:hover:text-gray-300">{{ .Name.Old }}</a></li>
22
-
{{ else }}
23
-
<li><a href="#file-{{ .Name.New }}" class="dark:hover:text-gray-300">{{ .Name.New }}</a></li>
24
-
{{ end }}
25
-
</ul>
26
-
{{ end }}
27
-
</div>
18
+
{{ block "fileTree" $fileTree }} {{ end }}
28
19
</div>
29
20
</section>
30
21
···
38
29
<summary class="list-none cursor-pointer sticky top-0">
39
30
<div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between">
40
31
<div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
41
-
<div class="flex gap-1 items-center" style="direction: ltr;">
32
+
<div class="flex gap-1 items-center">
42
33
{{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }}
43
34
{{ if .IsNew }}
44
35
<span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">ADDED</span>
···
55
46
{{ block "statPill" .Stats }} {{ end }}
56
47
</div>
57
48
58
-
<div class="flex gap-2 items-center overflow-x-auto" style="direction: rtl;">
49
+
<div class="flex gap-2 items-center overflow-x-auto">
59
50
{{ if .IsDelete }}
60
51
<a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this }}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.Old }}"{{end}}>
61
52
{{ .Name.Old }}
···
101
92
{{ else if .IsCopy }}
102
93
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
103
94
This file has been copied.
104
-
</p>
105
-
{{ else if .IsRename }}
106
-
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
107
-
This file has been renamed.
108
95
</p>
109
96
{{ else if .IsBinary }}
110
97
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
+27
appview/pages/templates/repo/fragments/filetree.html
+27
appview/pages/templates/repo/fragments/filetree.html
···
1
+
{{ define "fileTree" }}
2
+
{{ if and .Name .IsDirectory }}
3
+
<details open>
4
+
<summary class="cursor-pointer list-none pt-1">
5
+
<span class="inline-flex items-center gap-2 ">
6
+
{{ i "folder" "w-3 h-3 fill-current" }}
7
+
<span class="text-black dark:text-white">{{ .Name }}</span>
8
+
</span>
9
+
</summary>
10
+
<div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700">
11
+
{{ range $child := .Children }}
12
+
{{ block "fileTree" $child }} {{ end }}
13
+
{{ end }}
14
+
</div>
15
+
</details>
16
+
{{ else if .Name }}
17
+
<div class="flex items-center gap-2 pt-1">
18
+
{{ i "file" "w-3 h-3" }}
19
+
<a href="#file-{{ .Path }}" class="text-black dark:text-white no-underline hover:underline">{{ .Name }}</a>
20
+
</div>
21
+
{{ else }}
22
+
{{ range $child := .Children }}
23
+
{{ block "fileTree" $child }} {{ end }}
24
+
{{ end }}
25
+
{{ end }}
26
+
{{ end }}
27
+
+2
-7
appview/pages/templates/repo/fragments/interdiff.html
+2
-7
appview/pages/templates/repo/fragments/interdiff.html
···
1
1
{{ define "repo/fragments/interdiff" }}
2
2
{{ $repo := index . 0 }}
3
3
{{ $x := index . 1 }}
4
+
{{ $fileTree := fileTree $x.AffectedFiles }}
4
5
{{ $diff := $x.Files }}
5
6
6
7
<section class="mt-6 p-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm">
···
8
9
<div class="flex gap-2 items-center">
9
10
<strong class="text-sm uppercase dark:text-gray-200">files</strong>
10
11
</div>
11
-
<div class="overflow-x-auto">
12
-
<ul class="dark:text-gray-200">
13
-
{{ range $diff }}
14
-
<li><a href="#file-{{ .Name }}" class="dark:hover:text-gray-300">{{ .Name }}</a></li>
15
-
{{ end }}
16
-
</ul>
17
-
</div>
12
+
{{ block "fileTree" $fileTree }} {{ end }}
18
13
</div>
19
14
</section>
20
15
+38
appview/pages/templates/repo/issues/issues.html
+38
appview/pages/templates/repo/issues/issues.html
···
70
70
</div>
71
71
{{ end }}
72
72
</div>
73
+
74
+
{{ block "pagination" . }} {{ end }}
75
+
76
+
{{ end }}
77
+
78
+
{{ define "pagination" }}
79
+
<div class="flex justify-end mt-4 gap-2">
80
+
{{ $currentState := "closed" }}
81
+
{{ if .FilteringByOpen }}
82
+
{{ $currentState = "open" }}
83
+
{{ end }}
84
+
85
+
{{ if gt .Page.Offset 0 }}
86
+
{{ $prev := .Page.Previous }}
87
+
<a
88
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
89
+
hx-boost="true"
90
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}"
91
+
>
92
+
{{ i "chevron-left" "w-4 h-4" }}
93
+
previous
94
+
</a>
95
+
{{ else }}
96
+
<div></div>
97
+
{{ end }}
98
+
99
+
{{ if eq (len .Issues) .Page.Limit }}
100
+
{{ $next := .Page.Next }}
101
+
<a
102
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
103
+
hx-boost="true"
104
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}"
105
+
>
106
+
next
107
+
{{ i "chevron-right" "w-4 h-4" }}
108
+
</a>
109
+
{{ end }}
110
+
</div>
73
111
{{ end }}
+31
appview/pagination/page.go
+31
appview/pagination/page.go
···
1
+
package pagination
2
+
3
+
type Page struct {
4
+
Offset int // where to start from
5
+
Limit int // number of items in a page
6
+
}
7
+
8
+
func FirstPage() Page {
9
+
return Page{
10
+
Offset: 0,
11
+
Limit: 10,
12
+
}
13
+
}
14
+
15
+
func (p Page) Previous() Page {
16
+
if p.Offset-p.Limit < 0 {
17
+
return FirstPage()
18
+
} else {
19
+
return Page{
20
+
Offset: p.Offset - p.Limit,
21
+
Limit: p.Limit,
22
+
}
23
+
}
24
+
}
25
+
26
+
func (p Page) Next() Page {
27
+
return Page{
28
+
Offset: p.Offset + p.Limit,
29
+
Limit: p.Limit,
30
+
}
31
+
}
+3
-1
appview/state/pull.go
+3
-1
appview/state/pull.go
···
284
284
}
285
285
}
286
286
287
+
diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch)
288
+
287
289
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
288
290
LoggedInUser: user,
289
291
DidHandleMap: didHandleMap,
···
291
293
Pull: pull,
292
294
Round: roundIdInt,
293
295
Submission: pull.Submissions[roundIdInt],
294
-
Diff: pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch),
296
+
Diff: &diff,
295
297
})
296
298
297
299
}
+9
-1
appview/state/repo.go
+9
-1
appview/state/repo.go
···
28
28
"tangled.sh/tangled.sh/core/appview/db"
29
29
"tangled.sh/tangled.sh/core/appview/pages"
30
30
"tangled.sh/tangled.sh/core/appview/pages/markup"
31
+
"tangled.sh/tangled.sh/core/appview/pagination"
31
32
"tangled.sh/tangled.sh/core/types"
32
33
33
34
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
1559
1560
isOpen = true
1560
1561
}
1561
1562
1563
+
page, ok := r.Context().Value("page").(pagination.Page)
1564
+
if !ok {
1565
+
log.Println("failed to get page")
1566
+
page = pagination.FirstPage()
1567
+
}
1568
+
1562
1569
user := s.auth.GetUser(r)
1563
1570
f, err := fullyResolvedRepo(r)
1564
1571
if err != nil {
···
1566
1573
return
1567
1574
}
1568
1575
1569
-
issues, err := db.GetIssues(s.db, f.RepoAt, isOpen)
1576
+
issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page)
1570
1577
if err != nil {
1571
1578
log.Println("failed to get issues", err)
1572
1579
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
···
1593
1600
Issues: issues,
1594
1601
DidHandleMap: didHandleMap,
1595
1602
FilteringByOpen: isOpen,
1603
+
Page: page,
1596
1604
})
1597
1605
return
1598
1606
}
+1
-1
appview/state/router.go
+1
-1
appview/state/router.go
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
+10
-1
flake.nix
+10
-1
flake.nix
···
173
173
${pkgs.air}/bin/air -c /dev/null \
174
174
-build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \
175
175
-build.bin "./out/${name}.out" \
176
-
-build.include_ext "go,html,css"
176
+
-build.include_ext "go"
177
+
'';
178
+
tailwind-watcher =
179
+
pkgs.writeShellScriptBin "run"
180
+
''
181
+
${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css
177
182
'';
178
183
in {
179
184
watch-appview = {
···
183
188
watch-knotserver = {
184
189
type = "app";
185
190
program = ''${air-watcher "knotserver"}/bin/run'';
191
+
};
192
+
watch-tailwind = {
193
+
type = "app";
194
+
program = ''${tailwind-watcher}/bin/run'';
186
195
};
187
196
});
188
197
+8
patchutil/interdiff.go
+8
patchutil/interdiff.go
···
11
11
Files []*InterdiffFile
12
12
}
13
13
14
+
func (i *InterdiffResult) AffectedFiles() []string {
15
+
files := make([]string, len(i.Files))
16
+
for _, f := range i.Files {
17
+
files = append(files, f.Name)
18
+
}
19
+
return files
20
+
}
21
+
14
22
func (i *InterdiffResult) String() string {
15
23
var b strings.Builder
16
24
for _, f := range i.Files {
+14
types/diff.go
+14
types/diff.go
···
59
59
Patch string `json:"patch"`
60
60
Diff []*gitdiff.File `json:"diff"`
61
61
}
62
+
63
+
func (d *NiceDiff) ChangedFiles() []string {
64
+
files := make([]string, len(d.Diff))
65
+
66
+
for i, f := range d.Diff {
67
+
if f.IsDelete {
68
+
files[i] = f.Name.Old
69
+
} else {
70
+
files[i] = f.Name.New
71
+
}
72
+
}
73
+
74
+
return files
75
+
}