feat(web): collapse generated and large diff files with on-demand load
- commit
- 81bd47f1210167bc6d04ebacbd3b9b214275d798
- author
- brice · · 2026-10-20 08:00 UTC
- committer
- brice · · 2026-10-20 09:16 UTC
- parent
- 1b58f08
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
29 files · +1173 -475
M docs/merge-requests.md +6 -0
| @@ -63,6 +63,12 @@ In `mr show --json`, `mergeability` holds `state`, `behind`, `behind_capped`, `c | |||
| 63 | 63 | ||
| 64 | 64 | In the web merge box a mode that is not possible is disabled with its reason, and the Merge button too when no mode is possible. A merge the server refuses (target moved, conflict, branch moved) shows the page again with the reason in the box. | |
| 65 | 65 | ||
| 66 | + | ## Large diffs on the web | |
| 67 | + | ||
| 68 | + | The merge request, commit and compare pages list every changed file. Generated files (lockfiles, `vendor/`, `node_modules/`, minified or generated code) and large files (over 2,000 changed lines) are collapsed: the header and the counts show, the lines do not. "load diff" fetches that one file in place, in full, and you can comment on its lines. Without scripts the link reloads the page with that file loaded. After the first 25 files of a merge request, the other files load as you scroll. | |
| 69 | + | ||
| 70 | + | A file over 8 MiB of text cannot be shown in the browser: the page links to its raw blob. A commit page lists at most 300 files; clone the repository to read the rest. | |
| 71 | + | ||
| 66 | 72 | ## Notes | |
| 67 | 73 | ||
| 68 | 74 | A note is the only review object. It is published as soon as it is written: there is no draft and no submit step. | |
| @@ -63,6 +63,12 @@ In `mr show --json`, `mergeability` holds `state`, `behind`, `behind_capped`, `c | |||||
| 63 | 63 | ||||
| 64 | In the web merge box a mode that is not possible is disabled with its reason, and the Merge button too when no mode is possible. A merge the server refuses (target moved, conflict, branch moved) shows the page again with the reason in the box. | 64 | In the web merge box a mode that is not possible is disabled with its reason, and the Merge button too when no mode is possible. A merge the server refuses (target moved, conflict, branch moved) shows the page again with the reason in the box. | ||
| 65 | 65 | ||||
| 66 | + | ## Large diffs on the web | |||
| 67 | + | ||||
| 68 | + | The merge request, commit and compare pages list every changed file. Generated files (lockfiles, `vendor/`, `node_modules/`, minified or generated code) and large files (over 2,000 changed lines) are collapsed: the header and the counts show, the lines do not. "load diff" fetches that one file in place, in full, and you can comment on its lines. Without scripts the link reloads the page with that file loaded. After the first 25 files of a merge request, the other files load as you scroll. | |||
| 69 | + | ||||
| 70 | + | A file over 8 MiB of text cannot be shown in the browser: the page links to its raw blob. A commit page lists at most 300 files; clone the repository to read the rest. | |||
| 71 | + | ||||
| 66 | ## Notes | 72 | ## Notes | ||
| 67 | 73 | ||||
| 68 | A note is the only review object. It is published as soon as it is written: there is no draft and no submit step. | 74 | A note is the only review object. It is published as soon as it is written: there is no draft and no submit step. | ||
M internal/adapters/web/assets.go +2 -1
| @@ -24,7 +24,7 @@ const StaticPrefix = "/-/static/" | |||
| 24 | 24 | // Assets are the URLs of the assets a page loads. Their names carry a hash of | |
| 25 | 25 | // the content, so they can be cached for ever. | |
| 26 | 26 | type Assets struct { | |
| 27 | - | CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, MR, IssueForm, PowWorker, Icon string | |
| 27 | + | CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, Lazy, MR, IssueForm, PowWorker, Icon string | |
| 28 | 28 | // Manifest is the web app manifest (install on a phone's home screen), | |
| 29 | 29 | // AppleIcon the home screen icon iOS reads from a link instead. | |
| 30 | 30 | Manifest, AppleIcon string | |
| @@ -64,6 +64,7 @@ var scripts = []struct { | |||
| 64 | 64 | {"files.js", func(a *Assets, u string) { a.Files = u }}, | |
| 65 | 65 | {"passkey.js", func(a *Assets, u string) { a.Passkey = u }}, | |
| 66 | 66 | {"log.js", func(a *Assets, u string) { a.Log = u }}, | |
| 67 | + | {"lazy.js", func(a *Assets, u string) { a.Lazy = u }}, | |
| 67 | 68 | {"mr.js", func(a *Assets, u string) { a.MR = u }}, | |
| 68 | 69 | {"issue-form.js", func(a *Assets, u string) { a.IssueForm = u }}, | |
| 69 | 70 | {"pow-worker.js", func(a *Assets, u string) { a.PowWorker = u }}, | |
| @@ -24,7 +24,7 @@ const StaticPrefix = "/-/static/" | |||||
| 24 | // Assets are the URLs of the assets a page loads. Their names carry a hash of | 24 | // Assets are the URLs of the assets a page loads. Their names carry a hash of | ||
| 25 | // the content, so they can be cached for ever. | 25 | // the content, so they can be cached for ever. | ||
| 26 | type Assets struct { | 26 | type Assets struct { | ||
| 27 | - | CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, MR, IssueForm, PowWorker, Icon string | 27 | + | CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, Lazy, MR, IssueForm, PowWorker, Icon string |
| 28 | // Manifest is the web app manifest (install on a phone's home screen), | 28 | // Manifest is the web app manifest (install on a phone's home screen), | ||
| 29 | // AppleIcon the home screen icon iOS reads from a link instead. | 29 | // AppleIcon the home screen icon iOS reads from a link instead. | ||
| 30 | Manifest, AppleIcon string | 30 | Manifest, AppleIcon string | ||
| @@ -64,6 +64,7 @@ var scripts = []struct { | |||||
| 64 | {"files.js", func(a *Assets, u string) { a.Files = u }}, | 64 | {"files.js", func(a *Assets, u string) { a.Files = u }}, | ||
| 65 | {"passkey.js", func(a *Assets, u string) { a.Passkey = u }}, | 65 | {"passkey.js", func(a *Assets, u string) { a.Passkey = u }}, | ||
| 66 | {"log.js", func(a *Assets, u string) { a.Log = u }}, | 66 | {"log.js", func(a *Assets, u string) { a.Log = u }}, | ||
| 67 | + | {"lazy.js", func(a *Assets, u string) { a.Lazy = u }}, | |||
| 67 | {"mr.js", func(a *Assets, u string) { a.MR = u }}, | 68 | {"mr.js", func(a *Assets, u string) { a.MR = u }}, | ||
| 68 | {"issue-form.js", func(a *Assets, u string) { a.IssueForm = u }}, | 69 | {"issue-form.js", func(a *Assets, u string) { a.IssueForm = u }}, | ||
| 69 | {"pow-worker.js", func(a *Assets, u string) { a.PowWorker = u }}, | 70 | {"pow-worker.js", func(a *Assets, u string) { a.PowWorker = u }}, | ||
M internal/adapters/web/commit.go +80 -11
| @@ -106,7 +106,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||
| 106 | 106 | if err != nil { | |
| 107 | 107 | return nil, 0, err | |
| 108 | 108 | } | |
| 109 | - | res, err := s.getCommit(ctx, d.repo.Name, sha, ports.GetCommitOptions{}) | |
| 109 | + | res, err := s.commitDiff(ctx, r, d.repo.Name, sha) | |
| 110 | 110 | if err != nil { | |
| 111 | 111 | return nil, 0, err | |
| 112 | 112 | } | |
| @@ -129,6 +129,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||
| 129 | 129 | Verification: c.Verification, | |
| 130 | 130 | } | |
| 131 | 131 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", cid)) | |
| 132 | + | collapseFiles(p.Files, res.Diff.Files, commitLinks(d.name(), cid)) | |
| 132 | 133 | if cm := person(c.Committer, now); cm != p.Author || c.Committer.Email != c.Author.Email { | |
| 133 | 134 | p.Committer = &cm | |
| 134 | 135 | } | |
| @@ -167,6 +168,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||
| 167 | 168 | {Label: p.Short, Current: true}, | |
| 168 | 169 | } | |
| 169 | 170 | l.HasRail = true | |
| 171 | + | l.Lazy = true | |
| 170 | 172 | return Commit(l, p), http.StatusOK, nil | |
| 171 | 173 | } | |
| 172 | 174 | ||
| @@ -187,23 +189,90 @@ func commitBody(c domain.Commit) string { | |||
| 187 | 189 | return messageText(strings.TrimSpace(body)) | |
| 188 | 190 | } | |
| 189 | 191 | ||
| 190 | - | // diffNote says what is missing from an incomplete diff, "" for a complete one. | |
| 192 | + | // diffNote says what is missing from an incomplete diff, "" for a complete one: | |
| 193 | + | // files past the limit of the listing, files too large for the browser at all. | |
| 194 | + | // Collapsed files say so on their own line and load on their own. | |
| 191 | 195 | func diffNote(d domain.Diff, shown, total int) string { | |
| 192 | - | cut := 0 | |
| 196 | + | gone := 0 | |
| 193 | 197 | for _, f := range d.Files { | |
| 194 | - | if f.Omitted != domain.OmitNone { | |
| 195 | - | cut++ | |
| 198 | + | if f.Omitted == domain.OmitUnavailable { | |
| 199 | + | gone++ | |
| 196 | 200 | } | |
| 197 | 201 | } | |
| 198 | - | if !d.Truncated && cut == 0 { | |
| 199 | - | return "" | |
| 200 | - | } | |
| 201 | 202 | var parts []string | |
| 202 | - | if shown < total { | |
| 203 | + | if d.Truncated && shown < total { | |
| 203 | 204 | parts = append(parts, plural(total-shown, "file is", "files are")+" not listed") | |
| 204 | 205 | } | |
| 205 | - | if cut > 0 { | |
| 206 | - | parts = append(parts, plural(cut, "file is", "files are")+" too large to show") | |
| 206 | + | if gone > 0 { | |
| 207 | + | parts = append(parts, plural(gone, "file is", "files are")+" too large to show") | |
| 208 | + | } | |
| 209 | + | if len(parts) == 0 { | |
| 210 | + | return "" | |
| 207 | 211 | } | |
| 208 | 212 | return "this diff is incomplete: " + strings.Join(parts, ", ") + ". the totals above count everything; clone the repository to read the rest." | |
| 209 | 213 | } | |
| 214 | + | ||
| 215 | + | // commitWindow is how many files of a commit get their hunks read with the | |
| 216 | + | // page; the files past it are collapsed, and load on their own. | |
| 217 | + | const commitWindow = 300 | |
| 218 | + | ||
| 219 | + | // commitDiff reads a commit for its page (and the compare page): every file | |
| 220 | + | // listed, the hunks of the first commitWindow files read, generated files | |
| 221 | + | // collapsed. Files past the window are collapsed too. With ?file=i (the link | |
| 222 | + | // of a page without scripts) file i is read in full and replaces its entry. | |
| 223 | + | func (s *Server) commitDiff(ctx context.Context, r *http.Request, repo domain.RepoName, rev string) (domain.CommitDetail, error) { | |
| 224 | + | res, err := s.getCommit(ctx, repo, rev, ports.GetCommitOptions{Files: &ports.FileWindow{From: 0, To: commitWindow}}) | |
| 225 | + | if err != nil { | |
| 226 | + | return res, err | |
| 227 | + | } | |
| 228 | + | for i := commitWindow; i < len(res.Diff.Files); i++ { | |
| 229 | + | if f := &res.Diff.Files[i]; !f.Binary && f.Omitted == domain.OmitNone && f.Additions+f.Deletions > 0 { | |
| 230 | + | f.Omitted = domain.OmitBudget | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | if i, ok := fileParam(r, len(res.Diff.Files)); ok && res.Diff.Files[i].Omitted.Collapsed() { | |
| 234 | + | one, err := s.getCommit(ctx, repo, rev, ports.GetCommitOptions{Files: &ports.FileWindow{From: i, To: i + 1, Full: true}}) | |
| 235 | + | if err != nil { | |
| 236 | + | return res, err | |
| 237 | + | } | |
| 238 | + | if i < len(one.Diff.Files) { | |
| 239 | + | res.Diff.Files[i] = one.Diff.Files[i] | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | return res, nil | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | // commitLinks are the addresses a commit page loads its collapsed files from. | |
| 246 | + | func commitLinks(repo, rev string) fileLinks { | |
| 247 | + | return fileLinks{src: repoHref(repo, "commit", rev, "files"), repo: repo, rev: rev} | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | // commitFiles is GET /<repo>/commit/<sha>/files?one=1&from=i[&view=&layouts=]: | |
| 251 | + | // the one file i of the commit with its hunks, in full, as the one | |
| 252 | + | // details.dfile the page swaps for its placeholder. | |
| 253 | + | func (s *Server) commitFiles(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 254 | + | sha := r.PathValue("sha") | |
| 255 | + | i, err := parseOne(r) | |
| 256 | + | if err != nil || !isCommitID(sha) { | |
| 257 | + | return nil, 0, errNotFound | |
| 258 | + | } | |
| 259 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 260 | + | if err != nil { | |
| 261 | + | return nil, 0, err | |
| 262 | + | } | |
| 263 | + | res, err := s.getCommit(ctx, d.repo.Name, sha, ports.GetCommitOptions{Files: &ports.FileWindow{From: i, To: i + 1, Full: true}}) | |
| 264 | + | if err != nil { | |
| 265 | + | return nil, 0, err | |
| 266 | + | } | |
| 267 | + | if i >= len(res.Diff.Files) { | |
| 268 | + | return nil, 0, errNotFound | |
| 269 | + | } | |
| 270 | + | cid := res.Commit.ID.String() | |
| 271 | + | raw := res.Diff.Files[i : i+1] | |
| 272 | + | files := s.hl.files(ctx, raw) | |
| 273 | + | files[0].ID = "f" + itoa(i+1) | |
| 274 | + | contextGaps(files, raw, repoHref(d.name(), "context", cid)) | |
| 275 | + | collapseFiles(files, raw, commitLinks(d.name(), cid)) | |
| 276 | + | lines, _ := diffLines(files) | |
| 277 | + | return MRFiles(MRFilesPage{Files: files, DiffView: fragmentView(r, lines)}), http.StatusOK, nil | |
| 278 | + | } | |
| @@ -106,7 +106,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 106 | if err != nil { | 106 | if err != nil { | ||
| 107 | return nil, 0, err | 107 | return nil, 0, err | ||
| 108 | } | 108 | } | ||
| 109 | - | res, err := s.getCommit(ctx, d.repo.Name, sha, ports.GetCommitOptions{}) | 109 | + | res, err := s.commitDiff(ctx, r, d.repo.Name, sha) |
| 110 | if err != nil { | 110 | if err != nil { | ||
| 111 | return nil, 0, err | 111 | return nil, 0, err | ||
| 112 | } | 112 | } | ||
| @@ -129,6 +129,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 129 | Verification: c.Verification, | 129 | Verification: c.Verification, | ||
| 130 | } | 130 | } | ||
| 131 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", cid)) | 131 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", cid)) | ||
| 132 | + | collapseFiles(p.Files, res.Diff.Files, commitLinks(d.name(), cid)) | |||
| 132 | if cm := person(c.Committer, now); cm != p.Author || c.Committer.Email != c.Author.Email { | 133 | if cm := person(c.Committer, now); cm != p.Author || c.Committer.Email != c.Author.Email { | ||
| 133 | p.Committer = &cm | 134 | p.Committer = &cm | ||
| 134 | } | 135 | } | ||
| @@ -167,6 +168,7 @@ func (s *Server) commit(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 167 | {Label: p.Short, Current: true}, | 168 | {Label: p.Short, Current: true}, | ||
| 168 | } | 169 | } | ||
| 169 | l.HasRail = true | 170 | l.HasRail = true | ||
| 171 | + | l.Lazy = true | |||
| 170 | return Commit(l, p), http.StatusOK, nil | 172 | return Commit(l, p), http.StatusOK, nil | ||
| 171 | } | 173 | } | ||
| 172 | 174 | ||||
| @@ -187,23 +189,90 @@ func commitBody(c domain.Commit) string { | |||||
| 187 | return messageText(strings.TrimSpace(body)) | 189 | return messageText(strings.TrimSpace(body)) | ||
| 188 | } | 190 | } | ||
| 189 | 191 | ||||
| 190 | - | // diffNote says what is missing from an incomplete diff, "" for a complete one. | 192 | + | // diffNote says what is missing from an incomplete diff, "" for a complete one: |
| 193 | + | // files past the limit of the listing, files too large for the browser at all. | |||
| 194 | + | // Collapsed files say so on their own line and load on their own. | |||
| 191 | func diffNote(d domain.Diff, shown, total int) string { | 195 | func diffNote(d domain.Diff, shown, total int) string { | ||
| 192 | - | cut := 0 | 196 | + | gone := 0 |
| 193 | for _, f := range d.Files { | 197 | for _, f := range d.Files { | ||
| 194 | - | if f.Omitted != domain.OmitNone { | 198 | + | if f.Omitted == domain.OmitUnavailable { |
| 195 | - | cut++ | 199 | + | gone++ |
| 196 | } | 200 | } | ||
| 197 | } | 201 | } | ||
| 198 | - | if !d.Truncated && cut == 0 { | |||
| 199 | - | return "" | |||
| 200 | - | } | |||
| 201 | var parts []string | 202 | var parts []string | ||
| 202 | - | if shown < total { | 203 | + | if d.Truncated && shown < total { |
| 203 | parts = append(parts, plural(total-shown, "file is", "files are")+" not listed") | 204 | parts = append(parts, plural(total-shown, "file is", "files are")+" not listed") | ||
| 204 | } | 205 | } | ||
| 205 | - | if cut > 0 { | 206 | + | if gone > 0 { |
| 206 | - | parts = append(parts, plural(cut, "file is", "files are")+" too large to show") | 207 | + | parts = append(parts, plural(gone, "file is", "files are")+" too large to show") |
| 208 | + | } | |||
| 209 | + | if len(parts) == 0 { | |||
| 210 | + | return "" | |||
| 207 | } | 211 | } | ||
| 208 | return "this diff is incomplete: " + strings.Join(parts, ", ") + ". the totals above count everything; clone the repository to read the rest." | 212 | return "this diff is incomplete: " + strings.Join(parts, ", ") + ". the totals above count everything; clone the repository to read the rest." | ||
| 209 | } | 213 | } | ||
| 214 | + | ||||
| 215 | + | // commitWindow is how many files of a commit get their hunks read with the | |||
| 216 | + | // page; the files past it are collapsed, and load on their own. | |||
| 217 | + | const commitWindow = 300 | |||
| 218 | + | ||||
| 219 | + | // commitDiff reads a commit for its page (and the compare page): every file | |||
| 220 | + | // listed, the hunks of the first commitWindow files read, generated files | |||
| 221 | + | // collapsed. Files past the window are collapsed too. With ?file=i (the link | |||
| 222 | + | // of a page without scripts) file i is read in full and replaces its entry. | |||
| 223 | + | func (s *Server) commitDiff(ctx context.Context, r *http.Request, repo domain.RepoName, rev string) (domain.CommitDetail, error) { | |||
| 224 | + | res, err := s.getCommit(ctx, repo, rev, ports.GetCommitOptions{Files: &ports.FileWindow{From: 0, To: commitWindow}}) | |||
| 225 | + | if err != nil { | |||
| 226 | + | return res, err | |||
| 227 | + | } | |||
| 228 | + | for i := commitWindow; i < len(res.Diff.Files); i++ { | |||
| 229 | + | if f := &res.Diff.Files[i]; !f.Binary && f.Omitted == domain.OmitNone && f.Additions+f.Deletions > 0 { | |||
| 230 | + | f.Omitted = domain.OmitBudget | |||
| 231 | + | } | |||
| 232 | + | } | |||
| 233 | + | if i, ok := fileParam(r, len(res.Diff.Files)); ok && res.Diff.Files[i].Omitted.Collapsed() { | |||
| 234 | + | one, err := s.getCommit(ctx, repo, rev, ports.GetCommitOptions{Files: &ports.FileWindow{From: i, To: i + 1, Full: true}}) | |||
| 235 | + | if err != nil { | |||
| 236 | + | return res, err | |||
| 237 | + | } | |||
| 238 | + | if i < len(one.Diff.Files) { | |||
| 239 | + | res.Diff.Files[i] = one.Diff.Files[i] | |||
| 240 | + | } | |||
| 241 | + | } | |||
| 242 | + | return res, nil | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | // commitLinks are the addresses a commit page loads its collapsed files from. | |||
| 246 | + | func commitLinks(repo, rev string) fileLinks { | |||
| 247 | + | return fileLinks{src: repoHref(repo, "commit", rev, "files"), repo: repo, rev: rev} | |||
| 248 | + | } | |||
| 249 | + | ||||
| 250 | + | // commitFiles is GET /<repo>/commit/<sha>/files?one=1&from=i[&view=&layouts=]: | |||
| 251 | + | // the one file i of the commit with its hunks, in full, as the one | |||
| 252 | + | // details.dfile the page swaps for its placeholder. | |||
| 253 | + | func (s *Server) commitFiles(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |||
| 254 | + | sha := r.PathValue("sha") | |||
| 255 | + | i, err := parseOne(r) | |||
| 256 | + | if err != nil || !isCommitID(sha) { | |||
| 257 | + | return nil, 0, errNotFound | |||
| 258 | + | } | |||
| 259 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |||
| 260 | + | if err != nil { | |||
| 261 | + | return nil, 0, err | |||
| 262 | + | } | |||
| 263 | + | res, err := s.getCommit(ctx, d.repo.Name, sha, ports.GetCommitOptions{Files: &ports.FileWindow{From: i, To: i + 1, Full: true}}) | |||
| 264 | + | if err != nil { | |||
| 265 | + | return nil, 0, err | |||
| 266 | + | } | |||
| 267 | + | if i >= len(res.Diff.Files) { | |||
| 268 | + | return nil, 0, errNotFound | |||
| 269 | + | } | |||
| 270 | + | cid := res.Commit.ID.String() | |||
| 271 | + | raw := res.Diff.Files[i : i+1] | |||
| 272 | + | files := s.hl.files(ctx, raw) | |||
| 273 | + | files[0].ID = "f" + itoa(i+1) | |||
| 274 | + | contextGaps(files, raw, repoHref(d.name(), "context", cid)) | |||
| 275 | + | collapseFiles(files, raw, commitLinks(d.name(), cid)) | |||
| 276 | + | lines, _ := diffLines(files) | |||
| 277 | + | return MRFiles(MRFilesPage{Files: files, DiffView: fragmentView(r, lines)}), http.StatusOK, nil | |||
| 278 | + | } | |||
M internal/adapters/web/compare.go +3 -1
| @@ -76,13 +76,14 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, | |||
| 76 | 76 | ||
| 77 | 77 | // The exact diff: one commit on top of the tip of the target. | |
| 78 | 78 | if exact && !behind && len(ahead) == 1 { | |
| 79 | - | res, err := s.getCommit(ctx, d.repo.Name, ahead[0].ID.String(), ports.GetCommitOptions{}) | |
| 79 | + | res, err := s.commitDiff(ctx, r, d.repo.Name, ahead[0].ID.String()) | |
| 80 | 80 | if err != nil { | |
| 81 | 81 | return nil, 0, err | |
| 82 | 82 | } | |
| 83 | 83 | p.HasDiff = true | |
| 84 | 84 | p.Files = s.hl.files(ctx, res.Diff.Files) | |
| 85 | 85 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", res.Commit.ID.String())) | |
| 86 | + | collapseFiles(p.Files, res.Diff.Files, commitLinks(name, res.Commit.ID.String())) | |
| 86 | 87 | p.NFiles, p.Adds, p.Dels = res.Stats.FilesChanged, res.Stats.Additions, res.Stats.Deletions | |
| 87 | 88 | if n := diffNote(res.Diff, len(p.Files), p.NFiles); n != "" { | |
| 88 | 89 | p.Note = n | |
| @@ -131,6 +132,7 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, | |||
| 131 | 132 | Crumb{Label: p.Source, Current: true}, | |
| 132 | 133 | ) | |
| 133 | 134 | l.HasRail = true | |
| 135 | + | l.Lazy = true | |
| 134 | 136 | return Compare(l, p), http.StatusOK, nil | |
| 135 | 137 | } | |
| 136 | 138 | ||
| @@ -76,13 +76,14 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 76 | 76 | ||||
| 77 | // The exact diff: one commit on top of the tip of the target. | 77 | // The exact diff: one commit on top of the tip of the target. | ||
| 78 | if exact && !behind && len(ahead) == 1 { | 78 | if exact && !behind && len(ahead) == 1 { | ||
| 79 | - | res, err := s.getCommit(ctx, d.repo.Name, ahead[0].ID.String(), ports.GetCommitOptions{}) | 79 | + | res, err := s.commitDiff(ctx, r, d.repo.Name, ahead[0].ID.String()) |
| 80 | if err != nil { | 80 | if err != nil { | ||
| 81 | return nil, 0, err | 81 | return nil, 0, err | ||
| 82 | } | 82 | } | ||
| 83 | p.HasDiff = true | 83 | p.HasDiff = true | ||
| 84 | p.Files = s.hl.files(ctx, res.Diff.Files) | 84 | p.Files = s.hl.files(ctx, res.Diff.Files) | ||
| 85 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", res.Commit.ID.String())) | 85 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", res.Commit.ID.String())) | ||
| 86 | + | collapseFiles(p.Files, res.Diff.Files, commitLinks(name, res.Commit.ID.String())) | |||
| 86 | p.NFiles, p.Adds, p.Dels = res.Stats.FilesChanged, res.Stats.Additions, res.Stats.Deletions | 87 | p.NFiles, p.Adds, p.Dels = res.Stats.FilesChanged, res.Stats.Additions, res.Stats.Deletions | ||
| 87 | if n := diffNote(res.Diff, len(p.Files), p.NFiles); n != "" { | 88 | if n := diffNote(res.Diff, len(p.Files), p.NFiles); n != "" { | ||
| 88 | p.Note = n | 89 | p.Note = n | ||
| @@ -131,6 +132,7 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 131 | Crumb{Label: p.Source, Current: true}, | 132 | Crumb{Label: p.Source, Current: true}, | ||
| 132 | ) | 133 | ) | ||
| 133 | l.HasRail = true | 134 | l.HasRail = true | ||
| 135 | + | l.Lazy = true | |||
| 134 | return Compare(l, p), http.StatusOK, nil | 136 | return Compare(l, p), http.StatusOK, nil | ||
| 135 | } | 137 | } | ||
| 136 | 138 | ||||
M internal/adapters/web/diff.go +89 -3
| @@ -7,8 +7,10 @@ import ( | |||
| 7 | 7 | "fmt" | |
| 8 | 8 | "html" | |
| 9 | 9 | "io" | |
| 10 | + | "net/http" | |
| 10 | 11 | "net/url" | |
| 11 | 12 | "slices" | |
| 13 | + | "strconv" | |
| 12 | 14 | "strings" | |
| 13 | 15 | "unicode/utf8" | |
| 14 | 16 | ||
| @@ -123,6 +125,14 @@ type DiffFile struct { | |||
| 123 | 125 | Note string | |
| 124 | 126 | Open bool | |
| 125 | 127 | Hunks []DiffHunk | |
| 128 | + | // Manual is true for a collapsed file (generated, large, over the page | |
| 129 | + | // budget): no hunks, a placeholder with a "load diff" control that reads | |
| 130 | + | // that one file (Src, empty when it cannot be loaded in place; Href is the | |
| 131 | + | // page without scripts). | |
| 132 | + | Manual bool | |
| 133 | + | Href string | |
| 134 | + | // Raw is the address of the raw blob of a file too large to show. | |
| 135 | + | Raw string | |
| 126 | 136 | // Digits is the width of the largest line number of the file, at least 2 | |
| 127 | 137 | // and at most 7: the gutter is as wide as the file needs (class lnw-N). | |
| 128 | 138 | Digits int | |
| @@ -371,7 +381,8 @@ func diffFiles(files []domain.FileDiff) []DiffFile { | |||
| 371 | 381 | Add: f.Additions, Del: f.Deletions, Bin: f.Binary, | |
| 372 | 382 | // A file that is shown in full opens; a file with nothing to read | |
| 373 | 383 | // (binary, truncated) stays closed under its note. | |
| 374 | - | Open: len(f.Hunks) > 0 && f.Omitted == domain.OmitNone, | |
| 384 | + | Open: len(f.Hunks) > 0 && f.Omitted == domain.OmitNone, | |
| 385 | + | Manual: f.Omitted.Collapsed(), | |
| 375 | 386 | } | |
| 376 | 387 | df.Key = f.NewPath | |
| 377 | 388 | switch f.Status { | |
| @@ -406,8 +417,14 @@ func fileNote(f domain.FileDiff) string { | |||
| 406 | 417 | switch { | |
| 407 | 418 | case f.Binary: | |
| 408 | 419 | parts = append(parts, "binary file: no text diff") | |
| 409 | - | case f.Omitted != domain.OmitNone: | |
| 410 | - | parts = append(parts, fmt.Sprintf("diff too large to show (%d lines changed): collapsed", f.Additions+f.Deletions)) | |
| 420 | + | case f.Omitted == domain.OmitGenerated: | |
| 421 | + | parts = append(parts, "generated file: collapsed") | |
| 422 | + | case f.Omitted == domain.OmitLarge: | |
| 423 | + | parts = append(parts, fmt.Sprintf("large diff (%d lines changed): collapsed", f.Additions+f.Deletions)) | |
| 424 | + | case f.Omitted == domain.OmitBudget: | |
| 425 | + | parts = append(parts, "collapsed to keep the page light") | |
| 426 | + | case f.Omitted == domain.OmitUnavailable: | |
| 427 | + | parts = append(parts, "too large to show in the browser (over 8 MiB of text)") | |
| 411 | 428 | } | |
| 412 | 429 | if f.OldMode != 0 && f.NewMode != 0 && f.OldMode != f.NewMode { | |
| 413 | 430 | parts = append(parts, fmt.Sprintf("mode %06o → %06o", f.OldMode, f.NewMode)) | |
| @@ -449,3 +466,72 @@ func codeLine(ln DiffLine) templ.Component { | |||
| 449 | 466 | return err | |
| 450 | 467 | }) | |
| 451 | 468 | } | |
| 469 | + | ||
| 470 | + | // fileLinks says where the collapsed files of a diff are loaded from. | |
| 471 | + | type fileLinks struct { | |
| 472 | + | // src is the address of the fragment route of the diff ("" when files | |
| 473 | + | // cannot be loaded in place); one file is src plus one=1&from=i. | |
| 474 | + | src string | |
| 475 | + | // from is the window of the page, kept in the links of a page without | |
| 476 | + | // scripts. | |
| 477 | + | from int | |
| 478 | + | // repo and rev name the raw blob of a file that cannot be shown. | |
| 479 | + | repo, rev string | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | // oneFileSrc is the address of the fragment holding file i alone. | |
| 483 | + | func oneFileSrc(src string, i int) string { | |
| 484 | + | sep := "?" | |
| 485 | + | if strings.Contains(src, "?") { | |
| 486 | + | sep = "&" | |
| 487 | + | } | |
| 488 | + | return src + sep + "one=1&from=" + itoa(i) | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | // collapseFiles gives the collapsed files of a diff their load address, and | |
| 492 | + | // the files too large to show a link to their raw blob. Nothing is read. | |
| 493 | + | func collapseFiles(out []DiffFile, files []domain.FileDiff, l fileLinks) { | |
| 494 | + | for i, f := range files { | |
| 495 | + | if i >= len(out) { | |
| 496 | + | break | |
| 497 | + | } | |
| 498 | + | switch { | |
| 499 | + | case f.Omitted.Collapsed(): | |
| 500 | + | if l.src != "" { | |
| 501 | + | out[i].Src = oneFileSrc(l.src, i) | |
| 502 | + | } | |
| 503 | + | q := "?file=" + itoa(i) | |
| 504 | + | if l.from > 0 { | |
| 505 | + | q = "?from=" + itoa(l.from) + "&file=" + itoa(i) | |
| 506 | + | } | |
| 507 | + | out[i].Href = q + "#" + out[i].ID | |
| 508 | + | case f.Omitted == domain.OmitUnavailable && f.Status != domain.FileDeleted && l.rev != "": | |
| 509 | + | out[i].Raw = repoHref(l.repo, "raw", l.rev, f.NewPath) | |
| 510 | + | } | |
| 511 | + | } | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | // fileParam reads ?file= of a page without scripts: the index of a collapsed | |
| 515 | + | // file to read in full, in [0, n). | |
| 516 | + | func fileParam(r *http.Request, n int) (int, bool) { | |
| 517 | + | s := r.URL.Query().Get("file") | |
| 518 | + | i, err := strconv.Atoi(s) | |
| 519 | + | if err != nil || i < 0 || i >= n || strconv.Itoa(i) != s { | |
| 520 | + | return 0, false | |
| 521 | + | } | |
| 522 | + | return i, true | |
| 523 | + | } | |
| 524 | + | ||
| 525 | + | // fragmentView lays out a one-file fragment like the page that asks for it: | |
| 526 | + | // ?view= is the layout shown and ?layouts= says whether the page holds both. | |
| 527 | + | func fragmentView(r *http.Request, lines int) DiffView { | |
| 528 | + | v := newDiffView(r, lines, "", "") | |
| 529 | + | switch r.URL.Query().Get("layouts") { | |
| 530 | + | case "both": | |
| 531 | + | v.Both = true | |
| 532 | + | case "one": | |
| 533 | + | v.Both = false | |
| 534 | + | } | |
| 535 | + | v.ShowUnified, v.ShowSplit = v.Both || v.View == "unified", v.Both || v.View == "split" | |
| 536 | + | return v | |
| 537 | + | } | |
| @@ -7,8 +7,10 @@ import ( | |||||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | "html" | 8 | "html" | ||
| 9 | "io" | 9 | "io" | ||
| 10 | + | "net/http" | |||
| 10 | "net/url" | 11 | "net/url" | ||
| 11 | "slices" | 12 | "slices" | ||
| 13 | + | "strconv" | |||
| 12 | "strings" | 14 | "strings" | ||
| 13 | "unicode/utf8" | 15 | "unicode/utf8" | ||
| 14 | 16 | ||||
| @@ -123,6 +125,14 @@ type DiffFile struct { | |||||
| 123 | Note string | 125 | Note string | ||
| 124 | Open bool | 126 | Open bool | ||
| 125 | Hunks []DiffHunk | 127 | Hunks []DiffHunk | ||
| 128 | + | // Manual is true for a collapsed file (generated, large, over the page | |||
| 129 | + | // budget): no hunks, a placeholder with a "load diff" control that reads | |||
| 130 | + | // that one file (Src, empty when it cannot be loaded in place; Href is the | |||
| 131 | + | // page without scripts). | |||
| 132 | + | Manual bool | |||
| 133 | + | Href string | |||
| 134 | + | // Raw is the address of the raw blob of a file too large to show. | |||
| 135 | + | Raw string | |||
| 126 | // Digits is the width of the largest line number of the file, at least 2 | 136 | // Digits is the width of the largest line number of the file, at least 2 | ||
| 127 | // and at most 7: the gutter is as wide as the file needs (class lnw-N). | 137 | // and at most 7: the gutter is as wide as the file needs (class lnw-N). | ||
| 128 | Digits int | 138 | Digits int | ||
| @@ -371,7 +381,8 @@ func diffFiles(files []domain.FileDiff) []DiffFile { | |||||
| 371 | Add: f.Additions, Del: f.Deletions, Bin: f.Binary, | 381 | Add: f.Additions, Del: f.Deletions, Bin: f.Binary, | ||
| 372 | // A file that is shown in full opens; a file with nothing to read | 382 | // A file that is shown in full opens; a file with nothing to read | ||
| 373 | // (binary, truncated) stays closed under its note. | 383 | // (binary, truncated) stays closed under its note. | ||
| 374 | - | Open: len(f.Hunks) > 0 && f.Omitted == domain.OmitNone, | 384 | + | Open: len(f.Hunks) > 0 && f.Omitted == domain.OmitNone, |
| 385 | + | Manual: f.Omitted.Collapsed(), | |||
| 375 | } | 386 | } | ||
| 376 | df.Key = f.NewPath | 387 | df.Key = f.NewPath | ||
| 377 | switch f.Status { | 388 | switch f.Status { | ||
| @@ -406,8 +417,14 @@ func fileNote(f domain.FileDiff) string { | |||||
| 406 | switch { | 417 | switch { | ||
| 407 | case f.Binary: | 418 | case f.Binary: | ||
| 408 | parts = append(parts, "binary file: no text diff") | 419 | parts = append(parts, "binary file: no text diff") | ||
| 409 | - | case f.Omitted != domain.OmitNone: | 420 | + | case f.Omitted == domain.OmitGenerated: |
| 410 | - | parts = append(parts, fmt.Sprintf("diff too large to show (%d lines changed): collapsed", f.Additions+f.Deletions)) | 421 | + | parts = append(parts, "generated file: collapsed") |
| 422 | + | case f.Omitted == domain.OmitLarge: | |||
| 423 | + | parts = append(parts, fmt.Sprintf("large diff (%d lines changed): collapsed", f.Additions+f.Deletions)) | |||
| 424 | + | case f.Omitted == domain.OmitBudget: | |||
| 425 | + | parts = append(parts, "collapsed to keep the page light") | |||
| 426 | + | case f.Omitted == domain.OmitUnavailable: | |||
| 427 | + | parts = append(parts, "too large to show in the browser (over 8 MiB of text)") | |||
| 411 | } | 428 | } | ||
| 412 | if f.OldMode != 0 && f.NewMode != 0 && f.OldMode != f.NewMode { | 429 | if f.OldMode != 0 && f.NewMode != 0 && f.OldMode != f.NewMode { | ||
| 413 | parts = append(parts, fmt.Sprintf("mode %06o → %06o", f.OldMode, f.NewMode)) | 430 | parts = append(parts, fmt.Sprintf("mode %06o → %06o", f.OldMode, f.NewMode)) | ||
| @@ -449,3 +466,72 @@ func codeLine(ln DiffLine) templ.Component { | |||||
| 449 | return err | 466 | return err | ||
| 450 | }) | 467 | }) | ||
| 451 | } | 468 | } | ||
| 469 | + | ||||
| 470 | + | // fileLinks says where the collapsed files of a diff are loaded from. | |||
| 471 | + | type fileLinks struct { | |||
| 472 | + | // src is the address of the fragment route of the diff ("" when files | |||
| 473 | + | // cannot be loaded in place); one file is src plus one=1&from=i. | |||
| 474 | + | src string | |||
| 475 | + | // from is the window of the page, kept in the links of a page without | |||
| 476 | + | // scripts. | |||
| 477 | + | from int | |||
| 478 | + | // repo and rev name the raw blob of a file that cannot be shown. | |||
| 479 | + | repo, rev string | |||
| 480 | + | } | |||
| 481 | + | ||||
| 482 | + | // oneFileSrc is the address of the fragment holding file i alone. | |||
| 483 | + | func oneFileSrc(src string, i int) string { | |||
| 484 | + | sep := "?" | |||
| 485 | + | if strings.Contains(src, "?") { | |||
| 486 | + | sep = "&" | |||
| 487 | + | } | |||
| 488 | + | return src + sep + "one=1&from=" + itoa(i) | |||
| 489 | + | } | |||
| 490 | + | ||||
| 491 | + | // collapseFiles gives the collapsed files of a diff their load address, and | |||
| 492 | + | // the files too large to show a link to their raw blob. Nothing is read. | |||
| 493 | + | func collapseFiles(out []DiffFile, files []domain.FileDiff, l fileLinks) { | |||
| 494 | + | for i, f := range files { | |||
| 495 | + | if i >= len(out) { | |||
| 496 | + | break | |||
| 497 | + | } | |||
| 498 | + | switch { | |||
| 499 | + | case f.Omitted.Collapsed(): | |||
| 500 | + | if l.src != "" { | |||
| 501 | + | out[i].Src = oneFileSrc(l.src, i) | |||
| 502 | + | } | |||
| 503 | + | q := "?file=" + itoa(i) | |||
| 504 | + | if l.from > 0 { | |||
| 505 | + | q = "?from=" + itoa(l.from) + "&file=" + itoa(i) | |||
| 506 | + | } | |||
| 507 | + | out[i].Href = q + "#" + out[i].ID | |||
| 508 | + | case f.Omitted == domain.OmitUnavailable && f.Status != domain.FileDeleted && l.rev != "": | |||
| 509 | + | out[i].Raw = repoHref(l.repo, "raw", l.rev, f.NewPath) | |||
| 510 | + | } | |||
| 511 | + | } | |||
| 512 | + | } | |||
| 513 | + | ||||
| 514 | + | // fileParam reads ?file= of a page without scripts: the index of a collapsed | |||
| 515 | + | // file to read in full, in [0, n). | |||
| 516 | + | func fileParam(r *http.Request, n int) (int, bool) { | |||
| 517 | + | s := r.URL.Query().Get("file") | |||
| 518 | + | i, err := strconv.Atoi(s) | |||
| 519 | + | if err != nil || i < 0 || i >= n || strconv.Itoa(i) != s { | |||
| 520 | + | return 0, false | |||
| 521 | + | } | |||
| 522 | + | return i, true | |||
| 523 | + | } | |||
| 524 | + | ||||
| 525 | + | // fragmentView lays out a one-file fragment like the page that asks for it: | |||
| 526 | + | // ?view= is the layout shown and ?layouts= says whether the page holds both. | |||
| 527 | + | func fragmentView(r *http.Request, lines int) DiffView { | |||
| 528 | + | v := newDiffView(r, lines, "", "") | |||
| 529 | + | switch r.URL.Query().Get("layouts") { | |||
| 530 | + | case "both": | |||
| 531 | + | v.Both = true | |||
| 532 | + | case "one": | |||
| 533 | + | v.Both = false | |||
| 534 | + | } | |||
| 535 | + | v.ShowUnified, v.ShowSplit = v.Both || v.View == "unified", v.Both || v.View == "split" | |||
| 536 | + | return v | |||
| 537 | + | } | |||
M internal/adapters/web/diff.templ +10 -1
| @@ -237,12 +237,19 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 237 | 237 | } | |
| 238 | 238 | </summary> | |
| 239 | 239 | if f.Lazy { | |
| 240 | - | // A placeholder: mr.js fetches the window of the file when it comes | |
| 240 | + | // A placeholder: lazy.js fetches the window of the file when it comes | |
| 241 | 241 | // near the screen and swaps the files of the window in. Without | |
| 242 | 242 | // scripts the link opens the page of that window. | |
| 243 | 243 | <div class={ "dlazy", "dlazy-" + itoa(f.Height) } data-src={ f.Src } data-notes={ f.Comments }> | |
| 244 | 244 | <a href={ templ.SafeURL("?from=" + itoa(f.From) + "#" + f.ID) }>show this file</a> | |
| 245 | 245 | </div> | |
| 246 | + | } else if f.Manual { | |
| 247 | + | // A collapsed file (generated, large, over the page budget): no | |
| 248 | + | // rows. "load diff" reads this one file and swaps it in place | |
| 249 | + | // (lazy.js); without scripts the link reloads the page with the file. | |
| 250 | + | <div class="dlazy" data-src={ f.Src } data-manual> | |
| 251 | + | <a href={ templ.SafeURL(f.Href) }>load diff</a> | |
| 252 | + | </div> | |
| 246 | 253 | } else if len(f.Hunks) > 0 { | |
| 247 | 254 | <div class="dscroll"> | |
| 248 | 255 | if p.ShowUnified { | |
| @@ -306,6 +313,8 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 306 | 313 | </table> | |
| 307 | 314 | } | |
| 308 | 315 | </div> | |
| 316 | + | } else if f.Raw != "" { | |
| 317 | + | <p class="note pad">{ f.Note } <a href={ templ.SafeURL(f.Raw) }>view raw</a></p> | |
| 309 | 318 | } else { | |
| 310 | 319 | <p class="note pad"> | |
| 311 | 320 | if f.Note != "" { | |
| @@ -237,12 +237,19 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||||
| 237 | } | 237 | } | ||
| 238 | </summary> | 238 | </summary> | ||
| 239 | if f.Lazy { | 239 | if f.Lazy { | ||
| 240 | - | // A placeholder: mr.js fetches the window of the file when it comes | 240 | + | // A placeholder: lazy.js fetches the window of the file when it comes |
| 241 | // near the screen and swaps the files of the window in. Without | 241 | // near the screen and swaps the files of the window in. Without | ||
| 242 | // scripts the link opens the page of that window. | 242 | // scripts the link opens the page of that window. | ||
| 243 | <div class={ "dlazy", "dlazy-" + itoa(f.Height) } data-src={ f.Src } data-notes={ f.Comments }> | 243 | <div class={ "dlazy", "dlazy-" + itoa(f.Height) } data-src={ f.Src } data-notes={ f.Comments }> | ||
| 244 | <a href={ templ.SafeURL("?from=" + itoa(f.From) + "#" + f.ID) }>show this file</a> | 244 | <a href={ templ.SafeURL("?from=" + itoa(f.From) + "#" + f.ID) }>show this file</a> | ||
| 245 | </div> | 245 | </div> | ||
| 246 | + | } else if f.Manual { | |||
| 247 | + | // A collapsed file (generated, large, over the page budget): no | |||
| 248 | + | // rows. "load diff" reads this one file and swaps it in place | |||
| 249 | + | // (lazy.js); without scripts the link reloads the page with the file. | |||
| 250 | + | <div class="dlazy" data-src={ f.Src } data-manual> | |||
| 251 | + | <a href={ templ.SafeURL(f.Href) }>load diff</a> | |||
| 252 | + | </div> | |||
| 246 | } else if len(f.Hunks) > 0 { | 253 | } else if len(f.Hunks) > 0 { | ||
| 247 | <div class="dscroll"> | 254 | <div class="dscroll"> | ||
| 248 | if p.ShowUnified { | 255 | if p.ShowUnified { | ||
| @@ -306,6 +313,8 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||||
| 306 | </table> | 313 | </table> | ||
| 307 | } | 314 | } | ||
| 308 | </div> | 315 | </div> | ||
| 316 | + | } else if f.Raw != "" { | |||
| 317 | + | <p class="note pad">{ f.Note } <a href={ templ.SafeURL(f.Raw) }>view raw</a></p> | |||
| 309 | } else { | 318 | } else { | ||
| 310 | <p class="note pad"> | 319 | <p class="note pad"> | ||
| 311 | if f.Note != "" { | 320 | if f.Note != "" { | ||
M internal/adapters/web/diff_templ.go +235 -173 generated file: collapsed
M internal/adapters/web/diff_test.go +55 -1
| @@ -99,7 +99,10 @@ func TestFileNotes(t *testing.T) { | |||
| 99 | 99 | {"rename", domain.FileDiff{Status: domain.FileRenamed, Similarity: 90, OldMode: 0o100644, NewMode: 0o100644}, "renamed, 90% similar", true}, | |
| 100 | 100 | {"copy", domain.FileDiff{Status: domain.FileCopied, Similarity: 80}, "copied, 80% similar", true}, | |
| 101 | 101 | {"type change", domain.FileDiff{Status: domain.FileTypeChanged}, "type changed", true}, | |
| 102 | - | {"truncated", domain.FileDiff{Omitted: domain.OmitLarge, Additions: 2500}, "too large", false}, | |
| 102 | + | {"generated", domain.FileDiff{Omitted: domain.OmitGenerated, Additions: 5}, "generated file: collapsed", true}, | |
| 103 | + | {"large", domain.FileDiff{Omitted: domain.OmitLarge, Additions: 2000, Deletions: 500}, "large diff (2500 lines changed): collapsed", true}, | |
| 104 | + | {"budget", domain.FileDiff{Omitted: domain.OmitBudget, Additions: 40}, "collapsed to keep the page light", true}, | |
| 105 | + | {"unavailable", domain.FileDiff{Omitted: domain.OmitUnavailable, Additions: 40}, "too large to show in the browser (over 8 MiB of text)", true}, | |
| 103 | 106 | } | |
| 104 | 107 | for _, tt := range tests { | |
| 105 | 108 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -191,3 +194,54 @@ func TestContextGaps(t *testing.T) { | |||
| 191 | 194 | contextGaps(o, odd, "/r/context/c") | |
| 192 | 195 | assert.Nil(t, o[0].Hunks[0].Gap) | |
| 193 | 196 | } | |
| 197 | + | ||
| 198 | + | func TestDiffNote(t *testing.T) { | |
| 199 | + | t.Parallel() | |
| 200 | + | files := func(w ...domain.FileOmission) domain.Diff { | |
| 201 | + | var d domain.Diff | |
| 202 | + | for _, o := range w { | |
| 203 | + | d.Files = append(d.Files, domain.FileDiff{Omitted: o}) | |
| 204 | + | } | |
| 205 | + | return d | |
| 206 | + | } | |
| 207 | + | assert.Empty(t, diffNote(files(domain.OmitGenerated, domain.OmitLarge, domain.OmitBudget), 3, 3), "collapsed files have no global note") | |
| 208 | + | d := files(domain.OmitNone) | |
| 209 | + | d.Truncated = true | |
| 210 | + | assert.Equal(t, "this diff is incomplete: 299 files are not listed. the totals above count everything; clone the repository to read the rest.", diffNote(d, 1, 300)) | |
| 211 | + | assert.Equal(t, "this diff is incomplete: 2 files are too large to show. the totals above count everything; clone the repository to read the rest.", | |
| 212 | + | diffNote(files(domain.OmitUnavailable, domain.OmitLarge, domain.OmitUnavailable), 3, 3)) | |
| 213 | + | d = files(domain.OmitUnavailable) | |
| 214 | + | d.Truncated = true | |
| 215 | + | assert.Contains(t, diffNote(d, 1, 5), "4 files are not listed, 1 file is too large to show") | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | func TestCollapsedFileMarkup(t *testing.T) { | |
| 219 | + | t.Parallel() | |
| 220 | + | view := DiffView{ShowUnified: true} | |
| 221 | + | src := "/demo/commit/abc1234/files" | |
| 222 | + | for _, why := range []domain.FileOmission{domain.OmitGenerated, domain.OmitLarge, domain.OmitBudget} { | |
| 223 | + | raw := []domain.FileDiff{{Status: domain.FileModified, NewPath: "go.sum", Additions: 3, Omitted: why}} | |
| 224 | + | fs := diffFiles(raw) | |
| 225 | + | collapseFiles(fs, raw, fileLinks{src: src, repo: "demo", rev: "abc1234"}) | |
| 226 | + | out := renderToString(t, DiffFileView(fs[0], view)) | |
| 227 | + | assert.NotContains(t, out, " open", "omission %d", why) | |
| 228 | + | assert.Contains(t, out, `<div class="dlazy" data-src="/demo/commit/abc1234/files?one=1&from=0" data-manual>`) | |
| 229 | + | assert.Contains(t, out, `<a href="?file=0#f1">load diff</a>`) | |
| 230 | + | assert.NotContains(t, out, "<table") | |
| 231 | + | } | |
| 232 | + | raw := []domain.FileDiff{{Status: domain.FileModified, NewPath: "huge.txt", Additions: 3, Omitted: domain.OmitUnavailable}} | |
| 233 | + | fs := diffFiles(raw) | |
| 234 | + | collapseFiles(fs, raw, fileLinks{src: src, repo: "demo", rev: "abc1234"}) | |
| 235 | + | out := renderToString(t, DiffFileView(fs[0], view)) | |
| 236 | + | assert.Contains(t, out, "too large to show in the browser") | |
| 237 | + | assert.NotContains(t, out, "load diff") | |
| 238 | + | assert.NotContains(t, out, "data-manual") | |
| 239 | + | assert.Contains(t, out, `<a href="/demo/raw/abc1234/huge.txt">view raw</a>`) | |
| 240 | + | ||
| 241 | + | // Past the first window of a merge request the link keeps the window. | |
| 242 | + | raw = []domain.FileDiff{{NewPath: "a", Additions: 1, Omitted: domain.OmitLarge}} | |
| 243 | + | fs = diffFiles(raw) | |
| 244 | + | collapseFiles(fs, raw, fileLinks{src: "/d/merge-requests/1/files?base=b&head=h", from: 25}) | |
| 245 | + | assert.Equal(t, "/d/merge-requests/1/files?base=b&head=h&one=1&from=0", fs[0].Src) | |
| 246 | + | assert.Equal(t, "?from=25&file=0#f1", fs[0].Href) | |
| 247 | + | } | |
| @@ -99,7 +99,10 @@ func TestFileNotes(t *testing.T) { | |||||
| 99 | {"rename", domain.FileDiff{Status: domain.FileRenamed, Similarity: 90, OldMode: 0o100644, NewMode: 0o100644}, "renamed, 90% similar", true}, | 99 | {"rename", domain.FileDiff{Status: domain.FileRenamed, Similarity: 90, OldMode: 0o100644, NewMode: 0o100644}, "renamed, 90% similar", true}, | ||
| 100 | {"copy", domain.FileDiff{Status: domain.FileCopied, Similarity: 80}, "copied, 80% similar", true}, | 100 | {"copy", domain.FileDiff{Status: domain.FileCopied, Similarity: 80}, "copied, 80% similar", true}, | ||
| 101 | {"type change", domain.FileDiff{Status: domain.FileTypeChanged}, "type changed", true}, | 101 | {"type change", domain.FileDiff{Status: domain.FileTypeChanged}, "type changed", true}, | ||
| 102 | - | {"truncated", domain.FileDiff{Omitted: domain.OmitLarge, Additions: 2500}, "too large", false}, | 102 | + | {"generated", domain.FileDiff{Omitted: domain.OmitGenerated, Additions: 5}, "generated file: collapsed", true}, |
| 103 | + | {"large", domain.FileDiff{Omitted: domain.OmitLarge, Additions: 2000, Deletions: 500}, "large diff (2500 lines changed): collapsed", true}, | |||
| 104 | + | {"budget", domain.FileDiff{Omitted: domain.OmitBudget, Additions: 40}, "collapsed to keep the page light", true}, | |||
| 105 | + | {"unavailable", domain.FileDiff{Omitted: domain.OmitUnavailable, Additions: 40}, "too large to show in the browser (over 8 MiB of text)", true}, | |||
| 103 | } | 106 | } | ||
| 104 | for _, tt := range tests { | 107 | for _, tt := range tests { | ||
| 105 | t.Run(tt.name, func(t *testing.T) { | 108 | t.Run(tt.name, func(t *testing.T) { | ||
| @@ -191,3 +194,54 @@ func TestContextGaps(t *testing.T) { | |||||
| 191 | contextGaps(o, odd, "/r/context/c") | 194 | contextGaps(o, odd, "/r/context/c") | ||
| 192 | assert.Nil(t, o[0].Hunks[0].Gap) | 195 | assert.Nil(t, o[0].Hunks[0].Gap) | ||
| 193 | } | 196 | } | ||
| 197 | + | ||||
| 198 | + | func TestDiffNote(t *testing.T) { | |||
| 199 | + | t.Parallel() | |||
| 200 | + | files := func(w ...domain.FileOmission) domain.Diff { | |||
| 201 | + | var d domain.Diff | |||
| 202 | + | for _, o := range w { | |||
| 203 | + | d.Files = append(d.Files, domain.FileDiff{Omitted: o}) | |||
| 204 | + | } | |||
| 205 | + | return d | |||
| 206 | + | } | |||
| 207 | + | assert.Empty(t, diffNote(files(domain.OmitGenerated, domain.OmitLarge, domain.OmitBudget), 3, 3), "collapsed files have no global note") | |||
| 208 | + | d := files(domain.OmitNone) | |||
| 209 | + | d.Truncated = true | |||
| 210 | + | assert.Equal(t, "this diff is incomplete: 299 files are not listed. the totals above count everything; clone the repository to read the rest.", diffNote(d, 1, 300)) | |||
| 211 | + | assert.Equal(t, "this diff is incomplete: 2 files are too large to show. the totals above count everything; clone the repository to read the rest.", | |||
| 212 | + | diffNote(files(domain.OmitUnavailable, domain.OmitLarge, domain.OmitUnavailable), 3, 3)) | |||
| 213 | + | d = files(domain.OmitUnavailable) | |||
| 214 | + | d.Truncated = true | |||
| 215 | + | assert.Contains(t, diffNote(d, 1, 5), "4 files are not listed, 1 file is too large to show") | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | func TestCollapsedFileMarkup(t *testing.T) { | |||
| 219 | + | t.Parallel() | |||
| 220 | + | view := DiffView{ShowUnified: true} | |||
| 221 | + | src := "/demo/commit/abc1234/files" | |||
| 222 | + | for _, why := range []domain.FileOmission{domain.OmitGenerated, domain.OmitLarge, domain.OmitBudget} { | |||
| 223 | + | raw := []domain.FileDiff{{Status: domain.FileModified, NewPath: "go.sum", Additions: 3, Omitted: why}} | |||
| 224 | + | fs := diffFiles(raw) | |||
| 225 | + | collapseFiles(fs, raw, fileLinks{src: src, repo: "demo", rev: "abc1234"}) | |||
| 226 | + | out := renderToString(t, DiffFileView(fs[0], view)) | |||
| 227 | + | assert.NotContains(t, out, " open", "omission %d", why) | |||
| 228 | + | assert.Contains(t, out, `<div class="dlazy" data-src="/demo/commit/abc1234/files?one=1&from=0" data-manual>`) | |||
| 229 | + | assert.Contains(t, out, `<a href="?file=0#f1">load diff</a>`) | |||
| 230 | + | assert.NotContains(t, out, "<table") | |||
| 231 | + | } | |||
| 232 | + | raw := []domain.FileDiff{{Status: domain.FileModified, NewPath: "huge.txt", Additions: 3, Omitted: domain.OmitUnavailable}} | |||
| 233 | + | fs := diffFiles(raw) | |||
| 234 | + | collapseFiles(fs, raw, fileLinks{src: src, repo: "demo", rev: "abc1234"}) | |||
| 235 | + | out := renderToString(t, DiffFileView(fs[0], view)) | |||
| 236 | + | assert.Contains(t, out, "too large to show in the browser") | |||
| 237 | + | assert.NotContains(t, out, "load diff") | |||
| 238 | + | assert.NotContains(t, out, "data-manual") | |||
| 239 | + | assert.Contains(t, out, `<a href="/demo/raw/abc1234/huge.txt">view raw</a>`) | |||
| 240 | + | ||||
| 241 | + | // Past the first window of a merge request the link keeps the window. | |||
| 242 | + | raw = []domain.FileDiff{{NewPath: "a", Additions: 1, Omitted: domain.OmitLarge}} | |||
| 243 | + | fs = diffFiles(raw) | |||
| 244 | + | collapseFiles(fs, raw, fileLinks{src: "/d/merge-requests/1/files?base=b&head=h", from: 25}) | |||
| 245 | + | assert.Equal(t, "/d/merge-requests/1/files?base=b&head=h&one=1&from=0", fs[0].Src) | |||
| 246 | + | assert.Equal(t, "?from=25&file=0#f1", fs[0].Href) | |||
| 247 | + | } | |||
A internal/adapters/web/jstest/lazy.test.mjs +156 -0
| @@ -0,0 +1,156 @@ | |||
| 1 | + | // Tests of the loader of the files of a diff (static/js/lazy.js). Run with | |
| 2 | + | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |
| 3 | + | import assert from 'node:assert/strict'; | |
| 4 | + | import { test } from 'node:test'; | |
| 5 | + | import { readFileSync } from 'node:fs'; | |
| 6 | + | ||
| 7 | + | const SRC = readFileSync(new URL('../static/js/lazy.js', import.meta.url), 'utf8'); | |
| 8 | + | ||
| 9 | + | globalThis.window = globalThis; | |
| 10 | + | const saved = { document: globalThis.document, fetch: globalThis.fetch, IO: globalThis.IntersectionObserver }; | |
| 11 | + | ||
| 12 | + | // A placeholder: an element with a dataset; its link finds it back. | |
| 13 | + | function placeholder(src, manual) { | |
| 14 | + | const details = { id: 'f1', querySelector: () => null }; | |
| 15 | + | const ph = { dataset: manual ? { src, manual: '' } : { src }, setAttribute() {}, removeAttribute() {}, closest: (s) => (s === 'details' ? details : null) }; | |
| 16 | + | const link = { closest: (s) => (s === '.dlazy' ? ph : s === '.dlazy[data-manual] > a' && manual ? link : null) }; | |
| 17 | + | return { ph, link, details }; | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | async function setup(phs, io = true) { | |
| 21 | + | const observed = []; | |
| 22 | + | const urls = []; | |
| 23 | + | if (io) globalThis.IntersectionObserver = class { observe(p) { observed.push(p); } unobserve() {} disconnect() {} }; | |
| 24 | + | else delete globalThis.IntersectionObserver; | |
| 25 | + | globalThis.fetch = async (u) => { urls.push(u); return { status: 200, ok: true, text: async () => '' }; }; | |
| 26 | + | globalThis.document = { | |
| 27 | + | readyState: 'complete', | |
| 28 | + | querySelector: (s) => (s === '#diffs' ? { dataset: {} } : null), | |
| 29 | + | querySelectorAll: (s) => (s.includes(':not([data-manual])') ? phs.filter((p) => !('manual' in p.dataset)) : phs), | |
| 30 | + | getElementById: () => null, | |
| 31 | + | createElement: () => ({ content: { querySelectorAll: () => [] } }), | |
| 32 | + | addEventListener() {}, | |
| 33 | + | }; | |
| 34 | + | delete globalThis.ermiteLazy; | |
| 35 | + | (0, eval)(SRC); | |
| 36 | + | return { observed, urls }; | |
| 37 | + | } | |
| 38 | + | const settle = () => new Promise((r) => setTimeout(r, 0)); | |
| 39 | + | const restore = () => { | |
| 40 | + | for (const [k, v] of [['document', saved.document], ['fetch', saved.fetch], ['IntersectionObserver', saved.IO]]) { | |
| 41 | + | if (v === undefined) delete globalThis[k]; else globalThis[k] = v; | |
| 42 | + | } | |
| 43 | + | }; | |
| 44 | + | ||
| 45 | + | test('manual placeholders are not observed', async () => { | |
| 46 | + | const a = placeholder('/r/commit/abc/files?one=1&from=0', true); | |
| 47 | + | const b = placeholder('/r/merge-requests/1/files?from=25', false); | |
| 48 | + | try { | |
| 49 | + | const { observed, urls } = await setup([a.ph, b.ph]); | |
| 50 | + | assert.deepEqual(observed, [b.ph]); | |
| 51 | + | assert.deepEqual(urls, []); | |
| 52 | + | } finally { await settle(); restore(); } | |
| 53 | + | }); | |
| 54 | + | ||
| 55 | + | test('without IntersectionObserver only the automatic placeholders load', async () => { | |
| 56 | + | const a = placeholder('/m?one=1&from=0', true); | |
| 57 | + | const b = placeholder('/w?from=25', false); | |
| 58 | + | try { | |
| 59 | + | const { urls } = await setup([a.ph, b.ph], false); | |
| 60 | + | assert.deepEqual(urls, ['/w?from=25']); | |
| 61 | + | } finally { await settle(); restore(); } | |
| 62 | + | }); | |
| 63 | + | ||
| 64 | + | test('a click on "load diff" loads that one file and does not navigate', async () => { | |
| 65 | + | const a = placeholder('/r/commit/abc/files?one=1&from=3', true); | |
| 66 | + | try { | |
| 67 | + | const { urls } = await setup([a.ph]); | |
| 68 | + | let prevented = false; | |
| 69 | + | const used = window.ermiteLazy.onClick({ target: a.link, button: 0, preventDefault() { prevented = true; } }); | |
| 70 | + | assert.equal(used, true); | |
| 71 | + | assert.equal(prevented, true); | |
| 72 | + | assert.deepEqual(urls, ['/r/commit/abc/files?one=1&from=3']); | |
| 73 | + | } finally { await settle(); restore(); } | |
| 74 | + | }); | |
| 75 | + | ||
| 76 | + | test('Enter and Space on the link load it; other keys and modified clicks do not', async () => { | |
| 77 | + | const a = placeholder('/f?one=1&from=1', true); | |
| 78 | + | try { | |
| 79 | + | const { urls } = await setup([a.ph]); | |
| 80 | + | const key = (k, extra = {}) => window.ermiteLazy.onKey({ target: a.link, key: k, preventDefault() {}, ...extra }); | |
| 81 | + | assert.equal(key('a'), false); | |
| 82 | + | assert.equal(key('Enter', { ctrlKey: true }), false); | |
| 83 | + | assert.equal(window.ermiteLazy.onClick({ target: a.link, button: 0, ctrlKey: true, preventDefault() {} }), false); | |
| 84 | + | assert.equal(urls.length, 0); | |
| 85 | + | assert.equal(key('Enter'), true); | |
| 86 | + | assert.equal(key(' '), true); | |
| 87 | + | assert.equal(urls.length, 1, 'a file is fetched once'); | |
| 88 | + | } finally { await settle(); restore(); } | |
| 89 | + | }); | |
| 90 | + | ||
| 91 | + | test('a manual placeholder without an address is a plain link', async () => { | |
| 92 | + | const a = placeholder('', true); | |
| 93 | + | a.ph.dataset.src = ''; | |
| 94 | + | try { | |
| 95 | + | await setup([a.ph]); | |
| 96 | + | assert.equal(window.ermiteLazy.onClick({ target: a.link, button: 0, preventDefault() { assert.fail(); } }), false); | |
| 97 | + | } finally { await settle(); restore(); } | |
| 98 | + | }); | |
| 99 | + | ||
| 100 | + | test('onSwap runs after a window came in', async () => { | |
| 101 | + | const b = placeholder('/w?from=25', false); | |
| 102 | + | try { | |
| 103 | + | await setup([b.ph]); | |
| 104 | + | let n = 0; | |
| 105 | + | window.ermiteLazy.onSwap(() => n++); | |
| 106 | + | await window.ermiteLazy.load(b.ph); | |
| 107 | + | assert.equal(n, 1); | |
| 108 | + | } finally { await settle(); restore(); } | |
| 109 | + | }); | |
| 110 | + | ||
| 111 | + | test('reset drops the response of a load in flight and lets the same window load again', async () => { | |
| 112 | + | const b = placeholder('/w?from=25', false); | |
| 113 | + | try { | |
| 114 | + | await setup([b.ph]); | |
| 115 | + | let release; | |
| 116 | + | const gate = new Promise((r) => { release = r; }); | |
| 117 | + | let fetches = 0; | |
| 118 | + | globalThis.fetch = async () => { fetches++; await gate; return { status: 200, ok: true, text: async () => '' }; }; | |
| 119 | + | let n = 0; | |
| 120 | + | window.ermiteLazy.onSwap(() => n++); | |
| 121 | + | const first = window.ermiteLazy.load(b.ph); | |
| 122 | + | window.ermiteLazy.reset(); | |
| 123 | + | release(); | |
| 124 | + | await first; | |
| 125 | + | assert.equal(n, 0, 'the old generation does not swap'); | |
| 126 | + | await window.ermiteLazy.load(b.ph); | |
| 127 | + | assert.equal(fetches, 2); | |
| 128 | + | assert.equal(n, 1); | |
| 129 | + | } finally { await settle(); restore(); } | |
| 130 | + | }); | |
| 131 | + | ||
| 132 | + | test('reset clears the stale state; watch disconnects the previous observer', async () => { | |
| 133 | + | const b = placeholder('/w?from=25', false); | |
| 134 | + | try { | |
| 135 | + | await setup([b.ph]); | |
| 136 | + | const made = []; | |
| 137 | + | globalThis.IntersectionObserver = class { | |
| 138 | + | constructor() { this.off = false; made.push(this); } | |
| 139 | + | observe() {} unobserve() {} disconnect() { this.off = true; } | |
| 140 | + | }; | |
| 141 | + | globalThis.fetch = async () => ({ status: 409, ok: false, text: async () => 'moved' }); | |
| 142 | + | await window.ermiteLazy.load(b.ph); | |
| 143 | + | const urls = []; | |
| 144 | + | globalThis.fetch = async (u) => { urls.push(u); return { status: 200, ok: true, text: async () => '' }; }; | |
| 145 | + | await window.ermiteLazy.load(b.ph); | |
| 146 | + | assert.deepEqual(urls, [], 'stale: nothing loads'); | |
| 147 | + | window.ermiteLazy.watch(); | |
| 148 | + | window.ermiteLazy.watch(); | |
| 149 | + | assert.equal(made.length, 2); | |
| 150 | + | assert.ok(made[0].off && !made[1].off); | |
| 151 | + | window.ermiteLazy.reset(); | |
| 152 | + | assert.ok(made[1].off); | |
| 153 | + | await window.ermiteLazy.load(b.ph); | |
| 154 | + | assert.equal(urls.length, 1); | |
| 155 | + | } finally { await settle(); restore(); } | |
| 156 | + | }); | |
| @@ -0,0 +1,156 @@ | |||||
| 1 | + | // Tests of the loader of the files of a diff (static/js/lazy.js). Run with | |||
| 2 | + | // node --test 'internal/adapters/web/jstest/*.test.mjs' | |||
| 3 | + | import assert from 'node:assert/strict'; | |||
| 4 | + | import { test } from 'node:test'; | |||
| 5 | + | import { readFileSync } from 'node:fs'; | |||
| 6 | + | ||||
| 7 | + | const SRC = readFileSync(new URL('../static/js/lazy.js', import.meta.url), 'utf8'); | |||
| 8 | + | ||||
| 9 | + | globalThis.window = globalThis; | |||
| 10 | + | const saved = { document: globalThis.document, fetch: globalThis.fetch, IO: globalThis.IntersectionObserver }; | |||
| 11 | + | ||||
| 12 | + | // A placeholder: an element with a dataset; its link finds it back. | |||
| 13 | + | function placeholder(src, manual) { | |||
| 14 | + | const details = { id: 'f1', querySelector: () => null }; | |||
| 15 | + | const ph = { dataset: manual ? { src, manual: '' } : { src }, setAttribute() {}, removeAttribute() {}, closest: (s) => (s === 'details' ? details : null) }; | |||
| 16 | + | const link = { closest: (s) => (s === '.dlazy' ? ph : s === '.dlazy[data-manual] > a' && manual ? link : null) }; | |||
| 17 | + | return { ph, link, details }; | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | async function setup(phs, io = true) { | |||
| 21 | + | const observed = []; | |||
| 22 | + | const urls = []; | |||
| 23 | + | if (io) globalThis.IntersectionObserver = class { observe(p) { observed.push(p); } unobserve() {} disconnect() {} }; | |||
| 24 | + | else delete globalThis.IntersectionObserver; | |||
| 25 | + | globalThis.fetch = async (u) => { urls.push(u); return { status: 200, ok: true, text: async () => '' }; }; | |||
| 26 | + | globalThis.document = { | |||
| 27 | + | readyState: 'complete', | |||
| 28 | + | querySelector: (s) => (s === '#diffs' ? { dataset: {} } : null), | |||
| 29 | + | querySelectorAll: (s) => (s.includes(':not([data-manual])') ? phs.filter((p) => !('manual' in p.dataset)) : phs), | |||
| 30 | + | getElementById: () => null, | |||
| 31 | + | createElement: () => ({ content: { querySelectorAll: () => [] } }), | |||
| 32 | + | addEventListener() {}, | |||
| 33 | + | }; | |||
| 34 | + | delete globalThis.ermiteLazy; | |||
| 35 | + | (0, eval)(SRC); | |||
| 36 | + | return { observed, urls }; | |||
| 37 | + | } | |||
| 38 | + | const settle = () => new Promise((r) => setTimeout(r, 0)); | |||
| 39 | + | const restore = () => { | |||
| 40 | + | for (const [k, v] of [['document', saved.document], ['fetch', saved.fetch], ['IntersectionObserver', saved.IO]]) { | |||
| 41 | + | if (v === undefined) delete globalThis[k]; else globalThis[k] = v; | |||
| 42 | + | } | |||
| 43 | + | }; | |||
| 44 | + | ||||
| 45 | + | test('manual placeholders are not observed', async () => { | |||
| 46 | + | const a = placeholder('/r/commit/abc/files?one=1&from=0', true); | |||
| 47 | + | const b = placeholder('/r/merge-requests/1/files?from=25', false); | |||
| 48 | + | try { | |||
| 49 | + | const { observed, urls } = await setup([a.ph, b.ph]); | |||
| 50 | + | assert.deepEqual(observed, [b.ph]); | |||
| 51 | + | assert.deepEqual(urls, []); | |||
| 52 | + | } finally { await settle(); restore(); } | |||
| 53 | + | }); | |||
| 54 | + | ||||
| 55 | + | test('without IntersectionObserver only the automatic placeholders load', async () => { | |||
| 56 | + | const a = placeholder('/m?one=1&from=0', true); | |||
| 57 | + | const b = placeholder('/w?from=25', false); | |||
| 58 | + | try { | |||
| 59 | + | const { urls } = await setup([a.ph, b.ph], false); | |||
| 60 | + | assert.deepEqual(urls, ['/w?from=25']); | |||
| 61 | + | } finally { await settle(); restore(); } | |||
| 62 | + | }); | |||
| 63 | + | ||||
| 64 | + | test('a click on "load diff" loads that one file and does not navigate', async () => { | |||
| 65 | + | const a = placeholder('/r/commit/abc/files?one=1&from=3', true); | |||
| 66 | + | try { | |||
| 67 | + | const { urls } = await setup([a.ph]); | |||
| 68 | + | let prevented = false; | |||
| 69 | + | const used = window.ermiteLazy.onClick({ target: a.link, button: 0, preventDefault() { prevented = true; } }); | |||
| 70 | + | assert.equal(used, true); | |||
| 71 | + | assert.equal(prevented, true); | |||
| 72 | + | assert.deepEqual(urls, ['/r/commit/abc/files?one=1&from=3']); | |||
| 73 | + | } finally { await settle(); restore(); } | |||
| 74 | + | }); | |||
| 75 | + | ||||
| 76 | + | test('Enter and Space on the link load it; other keys and modified clicks do not', async () => { | |||
| 77 | + | const a = placeholder('/f?one=1&from=1', true); | |||
| 78 | + | try { | |||
| 79 | + | const { urls } = await setup([a.ph]); | |||
| 80 | + | const key = (k, extra = {}) => window.ermiteLazy.onKey({ target: a.link, key: k, preventDefault() {}, ...extra }); | |||
| 81 | + | assert.equal(key('a'), false); | |||
| 82 | + | assert.equal(key('Enter', { ctrlKey: true }), false); | |||
| 83 | + | assert.equal(window.ermiteLazy.onClick({ target: a.link, button: 0, ctrlKey: true, preventDefault() {} }), false); | |||
| 84 | + | assert.equal(urls.length, 0); | |||
| 85 | + | assert.equal(key('Enter'), true); | |||
| 86 | + | assert.equal(key(' '), true); | |||
| 87 | + | assert.equal(urls.length, 1, 'a file is fetched once'); | |||
| 88 | + | } finally { await settle(); restore(); } | |||
| 89 | + | }); | |||
| 90 | + | ||||
| 91 | + | test('a manual placeholder without an address is a plain link', async () => { | |||
| 92 | + | const a = placeholder('', true); | |||
| 93 | + | a.ph.dataset.src = ''; | |||
| 94 | + | try { | |||
| 95 | + | await setup([a.ph]); | |||
| 96 | + | assert.equal(window.ermiteLazy.onClick({ target: a.link, button: 0, preventDefault() { assert.fail(); } }), false); | |||
| 97 | + | } finally { await settle(); restore(); } | |||
| 98 | + | }); | |||
| 99 | + | ||||
| 100 | + | test('onSwap runs after a window came in', async () => { | |||
| 101 | + | const b = placeholder('/w?from=25', false); | |||
| 102 | + | try { | |||
| 103 | + | await setup([b.ph]); | |||
| 104 | + | let n = 0; | |||
| 105 | + | window.ermiteLazy.onSwap(() => n++); | |||
| 106 | + | await window.ermiteLazy.load(b.ph); | |||
| 107 | + | assert.equal(n, 1); | |||
| 108 | + | } finally { await settle(); restore(); } | |||
| 109 | + | }); | |||
| 110 | + | ||||
| 111 | + | test('reset drops the response of a load in flight and lets the same window load again', async () => { | |||
| 112 | + | const b = placeholder('/w?from=25', false); | |||
| 113 | + | try { | |||
| 114 | + | await setup([b.ph]); | |||
| 115 | + | let release; | |||
| 116 | + | const gate = new Promise((r) => { release = r; }); | |||
| 117 | + | let fetches = 0; | |||
| 118 | + | globalThis.fetch = async () => { fetches++; await gate; return { status: 200, ok: true, text: async () => '' }; }; | |||
| 119 | + | let n = 0; | |||
| 120 | + | window.ermiteLazy.onSwap(() => n++); | |||
| 121 | + | const first = window.ermiteLazy.load(b.ph); | |||
| 122 | + | window.ermiteLazy.reset(); | |||
| 123 | + | release(); | |||
| 124 | + | await first; | |||
| 125 | + | assert.equal(n, 0, 'the old generation does not swap'); | |||
| 126 | + | await window.ermiteLazy.load(b.ph); | |||
| 127 | + | assert.equal(fetches, 2); | |||
| 128 | + | assert.equal(n, 1); | |||
| 129 | + | } finally { await settle(); restore(); } | |||
| 130 | + | }); | |||
| 131 | + | ||||
| 132 | + | test('reset clears the stale state; watch disconnects the previous observer', async () => { | |||
| 133 | + | const b = placeholder('/w?from=25', false); | |||
| 134 | + | try { | |||
| 135 | + | await setup([b.ph]); | |||
| 136 | + | const made = []; | |||
| 137 | + | globalThis.IntersectionObserver = class { | |||
| 138 | + | constructor() { this.off = false; made.push(this); } | |||
| 139 | + | observe() {} unobserve() {} disconnect() { this.off = true; } | |||
| 140 | + | }; | |||
| 141 | + | globalThis.fetch = async () => ({ status: 409, ok: false, text: async () => 'moved' }); | |||
| 142 | + | await window.ermiteLazy.load(b.ph); | |||
| 143 | + | const urls = []; | |||
| 144 | + | globalThis.fetch = async (u) => { urls.push(u); return { status: 200, ok: true, text: async () => '' }; }; | |||
| 145 | + | await window.ermiteLazy.load(b.ph); | |||
| 146 | + | assert.deepEqual(urls, [], 'stale: nothing loads'); | |||
| 147 | + | window.ermiteLazy.watch(); | |||
| 148 | + | window.ermiteLazy.watch(); | |||
| 149 | + | assert.equal(made.length, 2); | |||
| 150 | + | assert.ok(made[0].off && !made[1].off); | |||
| 151 | + | window.ermiteLazy.reset(); | |||
| 152 | + | assert.ok(made[1].off); | |||
| 153 | + | await window.ermiteLazy.load(b.ph); | |||
| 154 | + | assert.equal(urls.length, 1); | |||
| 155 | + | } finally { await settle(); restore(); } | |||
| 156 | + | }); | |||
M internal/adapters/web/jstest/mr.test.mjs +1 -0
| @@ -5,6 +5,7 @@ import { test } from 'node:test'; | |||
| 5 | 5 | ||
| 6 | 6 | globalThis.window = globalThis; | |
| 7 | 7 | globalThis.document = { readyState: 'complete', querySelector: () => null, querySelectorAll: () => [] }; | |
| 8 | + | await import('../static/js/lazy.js'); | |
| 8 | 9 | await import('../static/js/mr.js'); | |
| 9 | 10 | const C = window.ermiteMR.core; | |
| 10 | 11 | ||
| @@ -5,6 +5,7 @@ import { test } from 'node:test'; | |||||
| 5 | 5 | ||||
| 6 | globalThis.window = globalThis; | 6 | globalThis.window = globalThis; | ||
| 7 | globalThis.document = { readyState: 'complete', querySelector: () => null, querySelectorAll: () => [] }; | 7 | globalThis.document = { readyState: 'complete', querySelector: () => null, querySelectorAll: () => [] }; | ||
| 8 | + | await import('../static/js/lazy.js'); | |||
| 8 | await import('../static/js/mr.js'); | 9 | await import('../static/js/mr.js'); | ||
| 9 | const C = window.ermiteMR.core; | 10 | const C = window.ermiteMR.core; | ||
| 10 | 11 | ||||
M internal/adapters/web/mr.go +19 -1
| @@ -569,6 +569,19 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 569 | 569 | if from > 0 && from >= len(res.Diff.Files) { | |
| 570 | 570 | return nil, 0, errNotFound | |
| 571 | 571 | } | |
| 572 | + | // A page without scripts: ?file=i reads that collapsed file in full. | |
| 573 | + | if i, ok := fileParam(r, len(res.Diff.Files)); ok && res.Diff.Files[i].Omitted.Collapsed() { | |
| 574 | + | one, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, ports.GetMergeRequestOptions{ | |
| 575 | + | SinceLastRead: since, DiffOnly: true, Base: res.DiffBaseID, Head: res.MergeRequest.HeadID, | |
| 576 | + | Files: &ports.FileWindow{From: i, To: i + 1, Full: true}, | |
| 577 | + | }) | |
| 578 | + | if err != nil { | |
| 579 | + | return nil, 0, err | |
| 580 | + | } | |
| 581 | + | if i < len(one.Diff.Files) { | |
| 582 | + | res.Diff.Files[i] = one.Diff.Files[i] | |
| 583 | + | } | |
| 584 | + | } | |
| 572 | 585 | m := res.MergeRequest | |
| 573 | 586 | signed := SignedIn(ctx) | |
| 574 | 587 | open := m.State == domain.MRStateOpen | |
| @@ -590,6 +603,11 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 590 | 603 | Files: s.hl.files(ctx, res.Diff.Files), | |
| 591 | 604 | } | |
| 592 | 605 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | |
| 606 | + | src := "" | |
| 607 | + | if !res.DiffBaseID.IsZero() { | |
| 608 | + | src = filesSrc(d.name(), n, since, res.DiffBaseID.String(), head) | |
| 609 | + | } | |
| 610 | + | collapseFiles(p.Files, res.Diff.Files, fileLinks{src: src, from: from, repo: d.name(), rev: head}) | |
| 593 | 611 | if err := s.mrFacts(ctx, r, d, &p, res); err != nil { | |
| 594 | 612 | return nil, 0, err | |
| 595 | 613 | } | |
| @@ -649,7 +667,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 649 | 667 | p.Live = true | |
| 650 | 668 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source)+"&head="+url.QueryEscape(head)+"&target="+url.QueryEscape(m.Target)) | |
| 651 | 669 | l.HasRail = true | |
| 652 | - | l.MR = true | |
| 670 | + | l.MR, l.Lazy = true, true | |
| 653 | 671 | return MergeRequest(l, p), http.StatusOK, nil | |
| 654 | 672 | } | |
| 655 | 673 | ||
| @@ -569,6 +569,19 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 569 | if from > 0 && from >= len(res.Diff.Files) { | 569 | if from > 0 && from >= len(res.Diff.Files) { | ||
| 570 | return nil, 0, errNotFound | 570 | return nil, 0, errNotFound | ||
| 571 | } | 571 | } | ||
| 572 | + | // A page without scripts: ?file=i reads that collapsed file in full. | |||
| 573 | + | if i, ok := fileParam(r, len(res.Diff.Files)); ok && res.Diff.Files[i].Omitted.Collapsed() { | |||
| 574 | + | one, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, ports.GetMergeRequestOptions{ | |||
| 575 | + | SinceLastRead: since, DiffOnly: true, Base: res.DiffBaseID, Head: res.MergeRequest.HeadID, | |||
| 576 | + | Files: &ports.FileWindow{From: i, To: i + 1, Full: true}, | |||
| 577 | + | }) | |||
| 578 | + | if err != nil { | |||
| 579 | + | return nil, 0, err | |||
| 580 | + | } | |||
| 581 | + | if i < len(one.Diff.Files) { | |||
| 582 | + | res.Diff.Files[i] = one.Diff.Files[i] | |||
| 583 | + | } | |||
| 584 | + | } | |||
| 572 | m := res.MergeRequest | 585 | m := res.MergeRequest | ||
| 573 | signed := SignedIn(ctx) | 586 | signed := SignedIn(ctx) | ||
| 574 | open := m.State == domain.MRStateOpen | 587 | open := m.State == domain.MRStateOpen | ||
| @@ -590,6 +603,11 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 590 | Files: s.hl.files(ctx, res.Diff.Files), | 603 | Files: s.hl.files(ctx, res.Diff.Files), | ||
| 591 | } | 604 | } | ||
| 592 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | 605 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head)) | ||
| 606 | + | src := "" | |||
| 607 | + | if !res.DiffBaseID.IsZero() { | |||
| 608 | + | src = filesSrc(d.name(), n, since, res.DiffBaseID.String(), head) | |||
| 609 | + | } | |||
| 610 | + | collapseFiles(p.Files, res.Diff.Files, fileLinks{src: src, from: from, repo: d.name(), rev: head}) | |||
| 593 | if err := s.mrFacts(ctx, r, d, &p, res); err != nil { | 611 | if err := s.mrFacts(ctx, r, d, &p, res); err != nil { | ||
| 594 | return nil, 0, err | 612 | return nil, 0, err | ||
| 595 | } | 613 | } | ||
| @@ -649,7 +667,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 649 | p.Live = true | 667 | p.Live = true | ||
| 650 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source)+"&head="+url.QueryEscape(head)+"&target="+url.QueryEscape(m.Target)) | 668 | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source)+"&head="+url.QueryEscape(head)+"&target="+url.QueryEscape(m.Target)) | ||
| 651 | l.HasRail = true | 669 | l.HasRail = true | ||
| 652 | - | l.MR = true | 670 | + | l.MR, l.Lazy = true, true |
| 653 | return MergeRequest(l, p), http.StatusOK, nil | 671 | return MergeRequest(l, p), http.StatusOK, nil | ||
| 654 | } | 672 | } | ||
| 655 | 673 | ||||
M internal/adapters/web/mr_files.go +30 -3
| @@ -73,6 +73,18 @@ func parseFrom(r *http.Request) (int, error) { | |||
| 73 | 73 | return n, nil | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | + | // parseOne reads ?one=1&from=i of a one-file fragment: the index i of the | |
| 77 | + | // file, any non-negative number. | |
| 78 | + | func parseOne(r *http.Request) (int, error) { | |
| 79 | + | q := r.URL.Query() | |
| 80 | + | s := q.Get("from") | |
| 81 | + | n, err := strconv.Atoi(s) | |
| 82 | + | if q.Get("one") != "1" || err != nil || n < 0 || strconv.Itoa(n) != s { | |
| 83 | + | return 0, errNotFound | |
| 84 | + | } | |
| 85 | + | return n, nil | |
| 86 | + | } | |
| 87 | + | ||
| 76 | 88 | // lazyFiles marks the files outside [from, from+mrWindow) lazy: the page shows | |
| 77 | 89 | // a placeholder that loads their window. src is the URL of /files with the | |
| 78 | 90 | // base and the head of the diff; self the page, for the links of a page | |
| @@ -167,7 +179,13 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||
| 167 | 179 | if since && !SignedIn(ctx) { | |
| 168 | 180 | return nil, 0, errNotFound | |
| 169 | 181 | } | |
| 170 | - | from, err := parseFrom(r) | |
| 182 | + | one := q.Get("one") == "1" | |
| 183 | + | var from int | |
| 184 | + | if one { | |
| 185 | + | from, err = parseOne(r) | |
| 186 | + | } else { | |
| 187 | + | from, err = parseFrom(r) | |
| 188 | + | } | |
| 171 | 189 | if err != nil { | |
| 172 | 190 | return nil, 0, err | |
| 173 | 191 | } | |
| @@ -180,9 +198,12 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||
| 180 | 198 | if err != nil { | |
| 181 | 199 | return nil, 0, err | |
| 182 | 200 | } | |
| 201 | + | win := &ports.FileWindow{From: from, To: from + mrWindow} | |
| 202 | + | if one { | |
| 203 | + | win = &ports.FileWindow{From: from, To: from + 1, Full: true} | |
| 204 | + | } | |
| 183 | 205 | res, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, ports.GetMergeRequestOptions{ | |
| 184 | - | SinceLastRead: since, DiffOnly: true, Base: base, Head: head, | |
| 185 | - | Files: &ports.FileWindow{From: from, To: from + mrWindow}, | |
| 206 | + | SinceLastRead: since, DiffOnly: true, Base: base, Head: head, Files: win, | |
| 186 | 207 | }) | |
| 187 | 208 | if errors.Is(err, domain.ErrConflict) { | |
| 188 | 209 | return MRFilesMoved(), http.StatusConflict, nil | |
| @@ -198,8 +219,14 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||
| 198 | 219 | canWrite := signed && m.State == domain.MRStateOpen && !since | |
| 199 | 220 | p := MRPage{Files: s.hl.files(ctx, res.Diff.Files), LooseInFiles: true, NoteAuthor: domain.AuthorName(principal(ctx))} | |
| 200 | 221 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String())) | |
| 222 | + | collapseFiles(p.Files, res.Diff.Files, fileLinks{ | |
| 223 | + | src: filesSrc(d.name(), n, since, base.String(), head.String()), from: from, repo: d.name(), rev: head.String(), | |
| 224 | + | }) | |
| 201 | 225 | s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil) | |
| 202 | 226 | window := p.Files[from:min(from+mrWindow, len(p.Files))] | |
| 227 | + | if one { | |
| 228 | + | window = p.Files[from : from+1] | |
| 229 | + | } | |
| 203 | 230 | // The layouts follow the page: it sends ?view= (the layout shown) and | |
| 204 | 231 | // ?layouts=both when it holds both layouts and switches them itself, so | |
| 205 | 232 | // that every window of the page renders the same way. | |
| @@ -73,6 +73,18 @@ func parseFrom(r *http.Request) (int, error) { | |||||
| 73 | return n, nil | 73 | return n, nil | ||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | + | // parseOne reads ?one=1&from=i of a one-file fragment: the index i of the | |||
| 77 | + | // file, any non-negative number. | |||
| 78 | + | func parseOne(r *http.Request) (int, error) { | |||
| 79 | + | q := r.URL.Query() | |||
| 80 | + | s := q.Get("from") | |||
| 81 | + | n, err := strconv.Atoi(s) | |||
| 82 | + | if q.Get("one") != "1" || err != nil || n < 0 || strconv.Itoa(n) != s { | |||
| 83 | + | return 0, errNotFound | |||
| 84 | + | } | |||
| 85 | + | return n, nil | |||
| 86 | + | } | |||
| 87 | + | ||||
| 76 | // lazyFiles marks the files outside [from, from+mrWindow) lazy: the page shows | 88 | // lazyFiles marks the files outside [from, from+mrWindow) lazy: the page shows | ||
| 77 | // a placeholder that loads their window. src is the URL of /files with the | 89 | // a placeholder that loads their window. src is the URL of /files with the | ||
| 78 | // base and the head of the diff; self the page, for the links of a page | 90 | // base and the head of the diff; self the page, for the links of a page | ||
| @@ -167,7 +179,13 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 167 | if since && !SignedIn(ctx) { | 179 | if since && !SignedIn(ctx) { | ||
| 168 | return nil, 0, errNotFound | 180 | return nil, 0, errNotFound | ||
| 169 | } | 181 | } | ||
| 170 | - | from, err := parseFrom(r) | 182 | + | one := q.Get("one") == "1" |
| 183 | + | var from int | |||
| 184 | + | if one { | |||
| 185 | + | from, err = parseOne(r) | |||
| 186 | + | } else { | |||
| 187 | + | from, err = parseFrom(r) | |||
| 188 | + | } | |||
| 171 | if err != nil { | 189 | if err != nil { | ||
| 172 | return nil, 0, err | 190 | return nil, 0, err | ||
| 173 | } | 191 | } | ||
| @@ -180,9 +198,12 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 180 | if err != nil { | 198 | if err != nil { | ||
| 181 | return nil, 0, err | 199 | return nil, 0, err | ||
| 182 | } | 200 | } | ||
| 201 | + | win := &ports.FileWindow{From: from, To: from + mrWindow} | |||
| 202 | + | if one { | |||
| 203 | + | win = &ports.FileWindow{From: from, To: from + 1, Full: true} | |||
| 204 | + | } | |||
| 183 | res, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, ports.GetMergeRequestOptions{ | 205 | res, err := s.cfg.MergeRequests.Get(ctx, principal(ctx), d.repo.Name, n, ports.GetMergeRequestOptions{ | ||
| 184 | - | SinceLastRead: since, DiffOnly: true, Base: base, Head: head, | 206 | + | SinceLastRead: since, DiffOnly: true, Base: base, Head: head, Files: win, |
| 185 | - | Files: &ports.FileWindow{From: from, To: from + mrWindow}, | |||
| 186 | }) | 207 | }) | ||
| 187 | if errors.Is(err, domain.ErrConflict) { | 208 | if errors.Is(err, domain.ErrConflict) { | ||
| 188 | return MRFilesMoved(), http.StatusConflict, nil | 209 | return MRFilesMoved(), http.StatusConflict, nil | ||
| @@ -198,8 +219,14 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component, | |||||
| 198 | canWrite := signed && m.State == domain.MRStateOpen && !since | 219 | canWrite := signed && m.State == domain.MRStateOpen && !since | ||
| 199 | p := MRPage{Files: s.hl.files(ctx, res.Diff.Files), LooseInFiles: true, NoteAuthor: domain.AuthorName(principal(ctx))} | 220 | p := MRPage{Files: s.hl.files(ctx, res.Diff.Files), LooseInFiles: true, NoteAuthor: domain.AuthorName(principal(ctx))} | ||
| 200 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String())) | 221 | contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String())) | ||
| 222 | + | collapseFiles(p.Files, res.Diff.Files, fileLinks{ | |||
| 223 | + | src: filesSrc(d.name(), n, since, base.String(), head.String()), from: from, repo: d.name(), rev: head.String(), | |||
| 224 | + | }) | |||
| 201 | s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil) | 225 | s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil) | ||
| 202 | window := p.Files[from:min(from+mrWindow, len(p.Files))] | 226 | window := p.Files[from:min(from+mrWindow, len(p.Files))] | ||
| 227 | + | if one { | |||
| 228 | + | window = p.Files[from : from+1] | |||
| 229 | + | } | |||
| 203 | // The layouts follow the page: it sends ?view= (the layout shown) and | 230 | // The layouts follow the page: it sends ?view= (the layout shown) and | ||
| 204 | // ?layouts=both when it holds both layouts and switches them itself, so | 231 | // ?layouts=both when it holds both layouts and switches them itself, so | ||
| 205 | // that every window of the page renders the same way. | 232 | // that every window of the page renders the same way. | ||
M internal/adapters/web/shell.templ +3 -0
| @@ -30,6 +30,9 @@ templ Shell(l Layout, rail templ.Component) { | |||
| 30 | 30 | <script src={ l.Assets.Files } defer></script> | |
| 31 | 31 | <script src={ l.Assets.Passkey } defer></script> | |
| 32 | 32 | <script src={ l.Assets.Log } defer></script> | |
| 33 | + | if l.Lazy { | |
| 34 | + | <script src={ l.Assets.Lazy } defer></script> | |
| 35 | + | } | |
| 33 | 36 | if l.MR { | |
| 34 | 37 | <script src={ l.Assets.MR } defer></script> | |
| 35 | 38 | } | |
| @@ -30,6 +30,9 @@ templ Shell(l Layout, rail templ.Component) { | |||||
| 30 | <script src={ l.Assets.Files } defer></script> | 30 | <script src={ l.Assets.Files } defer></script> | ||
| 31 | <script src={ l.Assets.Passkey } defer></script> | 31 | <script src={ l.Assets.Passkey } defer></script> | ||
| 32 | <script src={ l.Assets.Log } defer></script> | 32 | <script src={ l.Assets.Log } defer></script> | ||
| 33 | + | if l.Lazy { | |||
| 34 | + | <script src={ l.Assets.Lazy } defer></script> | |||
| 35 | + | } | |||
| 33 | if l.MR { | 36 | if l.MR { | ||
| 34 | <script src={ l.Assets.MR } defer></script> | 37 | <script src={ l.Assets.MR } defer></script> | ||
| 35 | } | 38 | } | ||
M internal/adapters/web/shell_templ.go +199 -180
| @@ -220,15 +220,15 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 220 | 220 | if templ_7745c5c3_Err != nil { | |
| 221 | 221 | return templ_7745c5c3_Err | |
| 222 | 222 | } | |
| 223 | - | if l.MR { | |
| 223 | + | if l.Lazy { | |
| 224 | 224 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<script src=\"") | |
| 225 | 225 | if templ_7745c5c3_Err != nil { | |
| 226 | 226 | return templ_7745c5c3_Err | |
| 227 | 227 | } | |
| 228 | 228 | var templ_7745c5c3_Var16 string | |
| 229 | - | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.MR) | |
| 229 | + | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Lazy) | |
| 230 | 230 | if templ_7745c5c3_Err != nil { | |
| 231 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 29} | |
| 231 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 31} | |
| 232 | 232 | } | |
| 233 | 233 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16) | |
| 234 | 234 | if templ_7745c5c3_Err != nil { | |
| @@ -239,15 +239,15 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 239 | 239 | return templ_7745c5c3_Err | |
| 240 | 240 | } | |
| 241 | 241 | } | |
| 242 | - | if l.IssueForm { | |
| 242 | + | if l.MR { | |
| 243 | 243 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script src=\"") | |
| 244 | 244 | if templ_7745c5c3_Err != nil { | |
| 245 | 245 | return templ_7745c5c3_Err | |
| 246 | 246 | } | |
| 247 | 247 | var templ_7745c5c3_Var17 string | |
| 248 | - | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.IssueForm) | |
| 248 | + | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.MR) | |
| 249 | 249 | if templ_7745c5c3_Err != nil { | |
| 250 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 37, Col: 36} | |
| 250 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 37, Col: 29} | |
| 251 | 251 | } | |
| 252 | 252 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) | |
| 253 | 253 | if templ_7745c5c3_Err != nil { | |
| @@ -258,69 +258,88 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 258 | 258 | return templ_7745c5c3_Err | |
| 259 | 259 | } | |
| 260 | 260 | } | |
| 261 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</head><body data-ref=\"") | |
| 261 | + | if l.IssueForm { | |
| 262 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<script src=\"") | |
| 263 | + | if templ_7745c5c3_Err != nil { | |
| 264 | + | return templ_7745c5c3_Err | |
| 265 | + | } | |
| 266 | + | var templ_7745c5c3_Var18 string | |
| 267 | + | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.IssueForm) | |
| 268 | + | if templ_7745c5c3_Err != nil { | |
| 269 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 40, Col: 36} | |
| 270 | + | } | |
| 271 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |
| 272 | + | if templ_7745c5c3_Err != nil { | |
| 273 | + | return templ_7745c5c3_Err | |
| 274 | + | } | |
| 275 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" defer></script>") | |
| 276 | + | if templ_7745c5c3_Err != nil { | |
| 277 | + | return templ_7745c5c3_Err | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</head><body data-ref=\"") | |
| 262 | 281 | if templ_7745c5c3_Err != nil { | |
| 263 | 282 | return templ_7745c5c3_Err | |
| 264 | 283 | } | |
| 265 | - | var templ_7745c5c3_Var18 string | |
| 266 | - | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath) | |
| 284 | + | var templ_7745c5c3_Var19 string | |
| 285 | + | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath) | |
| 267 | 286 | if templ_7745c5c3_Err != nil { | |
| 268 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 41, Col: 23} | |
| 287 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 44, Col: 23} | |
| 269 | 288 | } | |
| 270 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |
| 289 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) | |
| 271 | 290 | if templ_7745c5c3_Err != nil { | |
| 272 | 291 | return templ_7745c5c3_Err | |
| 273 | 292 | } | |
| 274 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" data-sha=\"") | |
| 293 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" data-sha=\"") | |
| 275 | 294 | if templ_7745c5c3_Err != nil { | |
| 276 | 295 | return templ_7745c5c3_Err | |
| 277 | 296 | } | |
| 278 | - | var templ_7745c5c3_Var19 string | |
| 279 | - | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA) | |
| 297 | + | var templ_7745c5c3_Var20 string | |
| 298 | + | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA) | |
| 280 | 299 | if templ_7745c5c3_Err != nil { | |
| 281 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 42, Col: 19} | |
| 300 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 45, Col: 19} | |
| 282 | 301 | } | |
| 283 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) | |
| 302 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) | |
| 284 | 303 | if templ_7745c5c3_Err != nil { | |
| 285 | 304 | return templ_7745c5c3_Err | |
| 286 | 305 | } | |
| 287 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-sel=\"\" data-nav=\"\"") | |
| 306 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-sel=\"\" data-nav=\"\"") | |
| 288 | 307 | if templ_7745c5c3_Err != nil { | |
| 289 | 308 | return templ_7745c5c3_Err | |
| 290 | 309 | } | |
| 291 | 310 | if l.PathsURL != "" { | |
| 292 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " data-paths=\"") | |
| 311 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " data-paths=\"") | |
| 293 | 312 | if templ_7745c5c3_Err != nil { | |
| 294 | 313 | return templ_7745c5c3_Err | |
| 295 | 314 | } | |
| 296 | - | var templ_7745c5c3_Var20 string | |
| 297 | - | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL) | |
| 315 | + | var templ_7745c5c3_Var21 string | |
| 316 | + | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL) | |
| 298 | 317 | if templ_7745c5c3_Err != nil { | |
| 299 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 46, Col: 27} | |
| 318 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 49, Col: 27} | |
| 300 | 319 | } | |
| 301 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) | |
| 320 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) | |
| 302 | 321 | if templ_7745c5c3_Err != nil { | |
| 303 | 322 | return templ_7745c5c3_Err | |
| 304 | 323 | } | |
| 305 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-blob=\"") | |
| 324 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" data-blob=\"") | |
| 306 | 325 | if templ_7745c5c3_Err != nil { | |
| 307 | 326 | return templ_7745c5c3_Err | |
| 308 | 327 | } | |
| 309 | - | var templ_7745c5c3_Var21 string | |
| 310 | - | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase) | |
| 328 | + | var templ_7745c5c3_Var22 string | |
| 329 | + | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase) | |
| 311 | 330 | if templ_7745c5c3_Err != nil { | |
| 312 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 47, Col: 26} | |
| 331 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 50, Col: 26} | |
| 313 | 332 | } | |
| 314 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) | |
| 333 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22) | |
| 315 | 334 | if templ_7745c5c3_Err != nil { | |
| 316 | 335 | return templ_7745c5c3_Err | |
| 317 | 336 | } | |
| 318 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"") | |
| 337 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"") | |
| 319 | 338 | if templ_7745c5c3_Err != nil { | |
| 320 | 339 | return templ_7745c5c3_Err | |
| 321 | 340 | } | |
| 322 | 341 | } | |
| 323 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "><a class=\"skip\" href=\"#main\">skip to content</a>") | |
| 342 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "><a class=\"skip\" href=\"#main\">skip to content</a>") | |
| 324 | 343 | if templ_7745c5c3_Err != nil { | |
| 325 | 344 | return templ_7745c5c3_Err | |
| 326 | 345 | } | |
| @@ -328,48 +347,48 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 328 | 347 | if templ_7745c5c3_Err != nil { | |
| 329 | 348 | return templ_7745c5c3_Err | |
| 330 | 349 | } | |
| 331 | - | var templ_7745c5c3_Var22 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)} | |
| 332 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) | |
| 350 | + | var templ_7745c5c3_Var23 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)} | |
| 351 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...) | |
| 333 | 352 | if templ_7745c5c3_Err != nil { | |
| 334 | 353 | return templ_7745c5c3_Err | |
| 335 | 354 | } | |
| 336 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"") | |
| 355 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"") | |
| 337 | 356 | if templ_7745c5c3_Err != nil { | |
| 338 | 357 | return templ_7745c5c3_Err | |
| 339 | 358 | } | |
| 340 | - | var templ_7745c5c3_Var23 string | |
| 341 | - | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var22).String()) | |
| 359 | + | var templ_7745c5c3_Var24 string | |
| 360 | + | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var23).String()) | |
| 342 | 361 | if templ_7745c5c3_Err != nil { | |
| 343 | 362 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0} | |
| 344 | 363 | } | |
| 345 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | |
| 364 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) | |
| 346 | 365 | if templ_7745c5c3_Err != nil { | |
| 347 | 366 | return templ_7745c5c3_Err | |
| 348 | 367 | } | |
| 349 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">") | |
| 368 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">") | |
| 350 | 369 | if templ_7745c5c3_Err != nil { | |
| 351 | 370 | return templ_7745c5c3_Err | |
| 352 | 371 | } | |
| 353 | 372 | if l.Lede != "" { | |
| 354 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">") | |
| 373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">") | |
| 355 | 374 | if templ_7745c5c3_Err != nil { | |
| 356 | 375 | return templ_7745c5c3_Err | |
| 357 | 376 | } | |
| 358 | - | var templ_7745c5c3_Var24 string | |
| 359 | - | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede) | |
| 377 | + | var templ_7745c5c3_Var25 string | |
| 378 | + | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede) | |
| 360 | 379 | if templ_7745c5c3_Err != nil { | |
| 361 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 54, Col: 81} | |
| 380 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 57, Col: 81} | |
| 362 | 381 | } | |
| 363 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) | |
| 382 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | |
| 364 | 383 | if templ_7745c5c3_Err != nil { | |
| 365 | 384 | return templ_7745c5c3_Err | |
| 366 | 385 | } | |
| 367 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</p></section>") | |
| 386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</p></section>") | |
| 368 | 387 | if templ_7745c5c3_Err != nil { | |
| 369 | 388 | return templ_7745c5c3_Err | |
| 370 | 389 | } | |
| 371 | 390 | } | |
| 372 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<main id=\"main\" tabindex=\"-1\">") | |
| 391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<main id=\"main\" tabindex=\"-1\">") | |
| 373 | 392 | if templ_7745c5c3_Err != nil { | |
| 374 | 393 | return templ_7745c5c3_Err | |
| 375 | 394 | } | |
| @@ -377,12 +396,12 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 377 | 396 | if templ_7745c5c3_Err != nil { | |
| 378 | 397 | return templ_7745c5c3_Err | |
| 379 | 398 | } | |
| 380 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</main>") | |
| 399 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</main>") | |
| 381 | 400 | if templ_7745c5c3_Err != nil { | |
| 382 | 401 | return templ_7745c5c3_Err | |
| 383 | 402 | } | |
| 384 | 403 | if l.HasRail && rail != nil { | |
| 385 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<aside class=\"rail\" aria-label=\"Context\">") | |
| 404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<aside class=\"rail\" aria-label=\"Context\">") | |
| 386 | 405 | if templ_7745c5c3_Err != nil { | |
| 387 | 406 | return templ_7745c5c3_Err | |
| 388 | 407 | } | |
| @@ -390,12 +409,12 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 390 | 409 | if templ_7745c5c3_Err != nil { | |
| 391 | 410 | return templ_7745c5c3_Err | |
| 392 | 411 | } | |
| 393 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</aside>") | |
| 412 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</aside>") | |
| 394 | 413 | if templ_7745c5c3_Err != nil { | |
| 395 | 414 | return templ_7745c5c3_Err | |
| 396 | 415 | } | |
| 397 | 416 | } | |
| 398 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>") | |
| 417 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div>") | |
| 399 | 418 | if templ_7745c5c3_Err != nil { | |
| 400 | 419 | return templ_7745c5c3_Err | |
| 401 | 420 | } | |
| @@ -423,7 +442,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 423 | 442 | if templ_7745c5c3_Err != nil { | |
| 424 | 443 | return templ_7745c5c3_Err | |
| 425 | 444 | } | |
| 426 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>") | |
| 445 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</body></html>") | |
| 427 | 446 | if templ_7745c5c3_Err != nil { | |
| 428 | 447 | return templ_7745c5c3_Err | |
| 429 | 448 | } | |
| @@ -449,366 +468,366 @@ func Header(l Layout) templ.Component { | |||
| 449 | 468 | }() | |
| 450 | 469 | } | |
| 451 | 470 | ctx = templ.InitializeContext(ctx) | |
| 452 | - | templ_7745c5c3_Var25 := templ.GetChildren(ctx) | |
| 453 | - | if templ_7745c5c3_Var25 == nil { | |
| 454 | - | templ_7745c5c3_Var25 = templ.NopComponent | |
| 471 | + | templ_7745c5c3_Var26 := templ.GetChildren(ctx) | |
| 472 | + | if templ_7745c5c3_Var26 == nil { | |
| 473 | + | templ_7745c5c3_Var26 = templ.NopComponent | |
| 455 | 474 | } | |
| 456 | 475 | ctx = templ.ClearChildren(ctx) | |
| 457 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ") | |
| 476 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ") | |
| 458 | 477 | if templ_7745c5c3_Err != nil { | |
| 459 | 478 | return templ_7745c5c3_Err | |
| 460 | 479 | } | |
| 461 | 480 | if l.Inbox != nil { | |
| 462 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a class=\"btn inbox-btn\" href=\"") | |
| 481 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a class=\"btn inbox-btn\" href=\"") | |
| 463 | 482 | if templ_7745c5c3_Err != nil { | |
| 464 | 483 | return templ_7745c5c3_Err | |
| 465 | 484 | } | |
| 466 | - | var templ_7745c5c3_Var26 templ.SafeURL | |
| 467 | - | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href)) | |
| 485 | + | var templ_7745c5c3_Var27 templ.SafeURL | |
| 486 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href)) | |
| 468 | 487 | if templ_7745c5c3_Err != nil { | |
| 469 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 86, Col: 39} | |
| 488 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 89, Col: 39} | |
| 470 | 489 | } | |
| 471 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 490 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | |
| 472 | 491 | if templ_7745c5c3_Err != nil { | |
| 473 | 492 | return templ_7745c5c3_Err | |
| 474 | 493 | } | |
| 475 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ") | |
| 494 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ") | |
| 476 | 495 | if templ_7745c5c3_Err != nil { | |
| 477 | 496 | return templ_7745c5c3_Err | |
| 478 | 497 | } | |
| 479 | 498 | if l.Inbox.Count >= 0 { | |
| 480 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<span class=\"count\">") | |
| 499 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<span class=\"count\">") | |
| 481 | 500 | if templ_7745c5c3_Err != nil { | |
| 482 | 501 | return templ_7745c5c3_Err | |
| 483 | 502 | } | |
| 484 | - | var templ_7745c5c3_Var27 string | |
| 485 | - | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count)) | |
| 503 | + | var templ_7745c5c3_Var28 string | |
| 504 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count)) | |
| 486 | 505 | if templ_7745c5c3_Err != nil { | |
| 487 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 92, Col: 47} | |
| 506 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 47} | |
| 488 | 507 | } | |
| 489 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | |
| 508 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | |
| 490 | 509 | if templ_7745c5c3_Err != nil { | |
| 491 | 510 | return templ_7745c5c3_Err | |
| 492 | 511 | } | |
| 493 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span>") | |
| 512 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</span>") | |
| 494 | 513 | if templ_7745c5c3_Err != nil { | |
| 495 | 514 | return templ_7745c5c3_Err | |
| 496 | 515 | } | |
| 497 | 516 | } | |
| 498 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a> ") | |
| 517 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a> ") | |
| 499 | 518 | if templ_7745c5c3_Err != nil { | |
| 500 | 519 | return templ_7745c5c3_Err | |
| 501 | 520 | } | |
| 502 | 521 | } | |
| 503 | 522 | if len(l.Crumbs) > 0 { | |
| 504 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>") | |
| 523 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>") | |
| 505 | 524 | if templ_7745c5c3_Err != nil { | |
| 506 | 525 | return templ_7745c5c3_Err | |
| 507 | 526 | } | |
| 508 | 527 | for _, c := range l.Crumbs { | |
| 509 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<li>") | |
| 528 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<li>") | |
| 510 | 529 | if templ_7745c5c3_Err != nil { | |
| 511 | 530 | return templ_7745c5c3_Err | |
| 512 | 531 | } | |
| 513 | 532 | if c.Href != "" { | |
| 514 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a href=\"") | |
| 533 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<a href=\"") | |
| 515 | 534 | if templ_7745c5c3_Err != nil { | |
| 516 | 535 | return templ_7745c5c3_Err | |
| 517 | 536 | } | |
| 518 | - | var templ_7745c5c3_Var28 templ.SafeURL | |
| 519 | - | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 537 | + | var templ_7745c5c3_Var29 templ.SafeURL | |
| 538 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 520 | 539 | if templ_7745c5c3_Err != nil { | |
| 521 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 103, Col: 38} | |
| 540 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 38} | |
| 522 | 541 | } | |
| 523 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | |
| 542 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 524 | 543 | if templ_7745c5c3_Err != nil { | |
| 525 | 544 | return templ_7745c5c3_Err | |
| 526 | 545 | } | |
| 527 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"") | |
| 546 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"") | |
| 528 | 547 | if templ_7745c5c3_Err != nil { | |
| 529 | 548 | return templ_7745c5c3_Err | |
| 530 | 549 | } | |
| 531 | 550 | if c.Current { | |
| 532 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " aria-current=\"page\"") | |
| 551 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " aria-current=\"page\"") | |
| 533 | 552 | if templ_7745c5c3_Err != nil { | |
| 534 | 553 | return templ_7745c5c3_Err | |
| 535 | 554 | } | |
| 536 | 555 | } | |
| 537 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, ">") | |
| 556 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">") | |
| 538 | 557 | if templ_7745c5c3_Err != nil { | |
| 539 | 558 | return templ_7745c5c3_Err | |
| 540 | 559 | } | |
| 541 | - | var templ_7745c5c3_Var29 string | |
| 542 | - | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 560 | + | var templ_7745c5c3_Var30 string | |
| 561 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 543 | 562 | if templ_7745c5c3_Err != nil { | |
| 544 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 107, Col: 19} | |
| 563 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 19} | |
| 545 | 564 | } | |
| 546 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 565 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 547 | 566 | if templ_7745c5c3_Err != nil { | |
| 548 | 567 | return templ_7745c5c3_Err | |
| 549 | 568 | } | |
| 550 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</a>") | |
| 569 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</a>") | |
| 551 | 570 | if templ_7745c5c3_Err != nil { | |
| 552 | 571 | return templ_7745c5c3_Err | |
| 553 | 572 | } | |
| 554 | 573 | } else { | |
| 555 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<span") | |
| 574 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span") | |
| 556 | 575 | if templ_7745c5c3_Err != nil { | |
| 557 | 576 | return templ_7745c5c3_Err | |
| 558 | 577 | } | |
| 559 | 578 | if c.Current { | |
| 560 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " aria-current=\"page\"") | |
| 579 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " aria-current=\"page\"") | |
| 561 | 580 | if templ_7745c5c3_Err != nil { | |
| 562 | 581 | return templ_7745c5c3_Err | |
| 563 | 582 | } | |
| 564 | 583 | } | |
| 565 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">") | |
| 584 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, ">") | |
| 566 | 585 | if templ_7745c5c3_Err != nil { | |
| 567 | 586 | return templ_7745c5c3_Err | |
| 568 | 587 | } | |
| 569 | - | var templ_7745c5c3_Var30 string | |
| 570 | - | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 588 | + | var templ_7745c5c3_Var31 string | |
| 589 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 571 | 590 | if templ_7745c5c3_Err != nil { | |
| 572 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 113, Col: 19} | |
| 591 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 116, Col: 19} | |
| 573 | 592 | } | |
| 574 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 593 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 575 | 594 | if templ_7745c5c3_Err != nil { | |
| 576 | 595 | return templ_7745c5c3_Err | |
| 577 | 596 | } | |
| 578 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span>") | |
| 597 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span>") | |
| 579 | 598 | if templ_7745c5c3_Err != nil { | |
| 580 | 599 | return templ_7745c5c3_Err | |
| 581 | 600 | } | |
| 582 | 601 | } | |
| 583 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</li>") | |
| 602 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</li>") | |
| 584 | 603 | if templ_7745c5c3_Err != nil { | |
| 585 | 604 | return templ_7745c5c3_Err | |
| 586 | 605 | } | |
| 587 | 606 | } | |
| 588 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</ol></nav>") | |
| 607 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</ol></nav>") | |
| 589 | 608 | if templ_7745c5c3_Err != nil { | |
| 590 | 609 | return templ_7745c5c3_Err | |
| 591 | 610 | } | |
| 592 | 611 | } | |
| 593 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"hdr-acts\">") | |
| 612 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"hdr-acts\">") | |
| 594 | 613 | if templ_7745c5c3_Err != nil { | |
| 595 | 614 | return templ_7745c5c3_Err | |
| 596 | 615 | } | |
| 597 | 616 | if l.Ref != "" { | |
| 598 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a class=\"btn ref\" href=\"") | |
| 617 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn ref\" href=\"") | |
| 599 | 618 | if templ_7745c5c3_Err != nil { | |
| 600 | 619 | return templ_7745c5c3_Err | |
| 601 | 620 | } | |
| 602 | - | var templ_7745c5c3_Var31 templ.SafeURL | |
| 603 | - | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs"))) | |
| 621 | + | var templ_7745c5c3_Var32 templ.SafeURL | |
| 622 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs"))) | |
| 604 | 623 | if templ_7745c5c3_Err != nil { | |
| 605 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 124, Col: 52} | |
| 624 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 52} | |
| 606 | 625 | } | |
| 607 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 626 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 608 | 627 | if templ_7745c5c3_Err != nil { | |
| 609 | 628 | return templ_7745c5c3_Err | |
| 610 | 629 | } | |
| 611 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">") | |
| 630 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">") | |
| 612 | 631 | if templ_7745c5c3_Err != nil { | |
| 613 | 632 | return templ_7745c5c3_Err | |
| 614 | 633 | } | |
| 615 | - | var templ_7745c5c3_Var32 string | |
| 616 | - | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref) | |
| 634 | + | var templ_7745c5c3_Var33 string | |
| 635 | + | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref) | |
| 617 | 636 | if templ_7745c5c3_Err != nil { | |
| 618 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 36} | |
| 637 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 36} | |
| 619 | 638 | } | |
| 620 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 639 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 621 | 640 | if templ_7745c5c3_Err != nil { | |
| 622 | 641 | return templ_7745c5c3_Err | |
| 623 | 642 | } | |
| 624 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span> <span class=\"sha\">") | |
| 643 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</span> <span class=\"sha\">") | |
| 625 | 644 | if templ_7745c5c3_Err != nil { | |
| 626 | 645 | return templ_7745c5c3_Err | |
| 627 | 646 | } | |
| 628 | - | var templ_7745c5c3_Var33 string | |
| 629 | - | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short) | |
| 647 | + | var templ_7745c5c3_Var34 string | |
| 648 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short) | |
| 630 | 649 | if templ_7745c5c3_Err != nil { | |
| 631 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 131, Col: 33} | |
| 650 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 33} | |
| 632 | 651 | } | |
| 633 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 652 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 634 | 653 | if templ_7745c5c3_Err != nil { | |
| 635 | 654 | return templ_7745c5c3_Err | |
| 636 | 655 | } | |
| 637 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></a> ") | |
| 656 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span></a> ") | |
| 638 | 657 | if templ_7745c5c3_Err != nil { | |
| 639 | 658 | return templ_7745c5c3_Err | |
| 640 | 659 | } | |
| 641 | 660 | } | |
| 642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ") | |
| 661 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ") | |
| 643 | 662 | if templ_7745c5c3_Err != nil { | |
| 644 | 663 | return templ_7745c5c3_Err | |
| 645 | 664 | } | |
| 646 | 665 | if SignedIn(ctx) { | |
| 647 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>") | |
| 666 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>") | |
| 648 | 667 | if templ_7745c5c3_Err != nil { | |
| 649 | 668 | return templ_7745c5c3_Err | |
| 650 | 669 | } | |
| 651 | - | var templ_7745c5c3_Var34 string | |
| 652 | - | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner) | |
| 670 | + | var templ_7745c5c3_Var35 string | |
| 671 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner) | |
| 653 | 672 | if templ_7745c5c3_Err != nil { | |
| 654 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 123} | |
| 673 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 123} | |
| 655 | 674 | } | |
| 656 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 675 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 657 | 676 | if templ_7745c5c3_Err != nil { | |
| 658 | 677 | return templ_7745c5c3_Err | |
| 659 | 678 | } | |
| 660 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> ") | |
| 679 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</span> ") | |
| 661 | 680 | if templ_7745c5c3_Err != nil { | |
| 662 | 681 | return templ_7745c5c3_Err | |
| 663 | 682 | } | |
| 664 | 683 | if l.Passkeys { | |
| 665 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ") | |
| 684 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ") | |
| 666 | 685 | if templ_7745c5c3_Err != nil { | |
| 667 | 686 | return templ_7745c5c3_Err | |
| 668 | 687 | } | |
| 669 | 688 | } | |
| 670 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>") | |
| 689 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>") | |
| 671 | 690 | if templ_7745c5c3_Err != nil { | |
| 672 | 691 | return templ_7745c5c3_Err | |
| 673 | 692 | } | |
| 674 | 693 | } else if l.Passkeys { | |
| 675 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>") | |
| 694 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>") | |
| 676 | 695 | if templ_7745c5c3_Err != nil { | |
| 677 | 696 | return templ_7745c5c3_Err | |
| 678 | 697 | } | |
| 679 | 698 | } | |
| 680 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div></div>") | |
| 699 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</div></div>") | |
| 681 | 700 | if templ_7745c5c3_Err != nil { | |
| 682 | 701 | return templ_7745c5c3_Err | |
| 683 | 702 | } | |
| 684 | 703 | if len(l.Sections) > 0 { | |
| 685 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>") | |
| 704 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>") | |
| 686 | 705 | if templ_7745c5c3_Err != nil { | |
| 687 | 706 | return templ_7745c5c3_Err | |
| 688 | 707 | } | |
| 689 | 708 | for _, s := range l.Sections { | |
| 690 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<li><a href=\"") | |
| 709 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<li><a href=\"") | |
| 691 | 710 | if templ_7745c5c3_Err != nil { | |
| 692 | 711 | return templ_7745c5c3_Err | |
| 693 | 712 | } | |
| 694 | - | var templ_7745c5c3_Var35 templ.SafeURL | |
| 695 | - | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | |
| 713 | + | var templ_7745c5c3_Var36 templ.SafeURL | |
| 714 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | |
| 696 | 715 | if templ_7745c5c3_Err != nil { | |
| 697 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 36} | |
| 716 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 170, Col: 36} | |
| 698 | 717 | } | |
| 699 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 718 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |
| 700 | 719 | if templ_7745c5c3_Err != nil { | |
| 701 | 720 | return templ_7745c5c3_Err | |
| 702 | 721 | } | |
| 703 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" data-key=\"") | |
| 722 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" data-key=\"") | |
| 704 | 723 | if templ_7745c5c3_Err != nil { | |
| 705 | 724 | return templ_7745c5c3_Err | |
| 706 | 725 | } | |
| 707 | - | var templ_7745c5c3_Var36 string | |
| 708 | - | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 726 | + | var templ_7745c5c3_Var37 string | |
| 727 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 709 | 728 | if templ_7745c5c3_Err != nil { | |
| 710 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 168, Col: 24} | |
| 729 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 171, Col: 24} | |
| 711 | 730 | } | |
| 712 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) | |
| 731 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | |
| 713 | 732 | if templ_7745c5c3_Err != nil { | |
| 714 | 733 | return templ_7745c5c3_Err | |
| 715 | 734 | } | |
| 716 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" aria-keyshortcuts=\"") | |
| 735 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" aria-keyshortcuts=\"") | |
| 717 | 736 | if templ_7745c5c3_Err != nil { | |
| 718 | 737 | return templ_7745c5c3_Err | |
| 719 | 738 | } | |
| 720 | - | var templ_7745c5c3_Var37 string | |
| 721 | - | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 739 | + | var templ_7745c5c3_Var38 string | |
| 740 | + | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 722 | 741 | if templ_7745c5c3_Err != nil { | |
| 723 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 33} | |
| 742 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 172, Col: 33} | |
| 724 | 743 | } | |
| 725 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | |
| 744 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38) | |
| 726 | 745 | if templ_7745c5c3_Err != nil { | |
| 727 | 746 | return templ_7745c5c3_Err | |
| 728 | 747 | } | |
| 729 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\"") | |
| 748 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\"") | |
| 730 | 749 | if templ_7745c5c3_Err != nil { | |
| 731 | 750 | return templ_7745c5c3_Err | |
| 732 | 751 | } | |
| 733 | 752 | if s.Current { | |
| 734 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " aria-current=\"page\"") | |
| 753 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " aria-current=\"page\"") | |
| 735 | 754 | if templ_7745c5c3_Err != nil { | |
| 736 | 755 | return templ_7745c5c3_Err | |
| 737 | 756 | } | |
| 738 | 757 | } | |
| 739 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, ">") | |
| 758 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, ">") | |
| 740 | 759 | if templ_7745c5c3_Err != nil { | |
| 741 | 760 | return templ_7745c5c3_Err | |
| 742 | 761 | } | |
| 743 | - | var templ_7745c5c3_Var38 string | |
| 744 | - | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | |
| 762 | + | var templ_7745c5c3_Var39 string | |
| 763 | + | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | |
| 745 | 764 | if templ_7745c5c3_Err != nil { | |
| 746 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 174, Col: 17} | |
| 765 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 177, Col: 17} | |
| 747 | 766 | } | |
| 748 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) | |
| 767 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 749 | 768 | if templ_7745c5c3_Err != nil { | |
| 750 | 769 | return templ_7745c5c3_Err | |
| 751 | 770 | } | |
| 752 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ") | |
| 771 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ") | |
| 753 | 772 | if templ_7745c5c3_Err != nil { | |
| 754 | 773 | return templ_7745c5c3_Err | |
| 755 | 774 | } | |
| 756 | 775 | if s.Count != "" { | |
| 757 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<span class=\"count\">") | |
| 776 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<span class=\"count\">") | |
| 758 | 777 | if templ_7745c5c3_Err != nil { | |
| 759 | 778 | return templ_7745c5c3_Err | |
| 760 | 779 | } | |
| 761 | - | var templ_7745c5c3_Var39 string | |
| 762 | - | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count) | |
| 780 | + | var templ_7745c5c3_Var40 string | |
| 781 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count) | |
| 763 | 782 | if templ_7745c5c3_Err != nil { | |
| 764 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 38} | |
| 783 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 38} | |
| 765 | 784 | } | |
| 766 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 785 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 767 | 786 | if templ_7745c5c3_Err != nil { | |
| 768 | 787 | return templ_7745c5c3_Err | |
| 769 | 788 | } | |
| 770 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<span class=\"sr\">") | |
| 789 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<span class=\"sr\">") | |
| 771 | 790 | if templ_7745c5c3_Err != nil { | |
| 772 | 791 | return templ_7745c5c3_Err | |
| 773 | 792 | } | |
| 774 | - | var templ_7745c5c3_Var40 string | |
| 775 | - | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(" open") | |
| 793 | + | var templ_7745c5c3_Var41 string | |
| 794 | + | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(" open") | |
| 776 | 795 | if templ_7745c5c3_Err != nil { | |
| 777 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 66} | |
| 796 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 66} | |
| 778 | 797 | } | |
| 779 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | |
| 798 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | |
| 780 | 799 | if templ_7745c5c3_Err != nil { | |
| 781 | 800 | return templ_7745c5c3_Err | |
| 782 | 801 | } | |
| 783 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</span></span> ") | |
| 802 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</span></span> ") | |
| 784 | 803 | if templ_7745c5c3_Err != nil { | |
| 785 | 804 | return templ_7745c5c3_Err | |
| 786 | 805 | } | |
| 787 | 806 | } | |
| 788 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<kbd aria-hidden=\"true\">") | |
| 807 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<kbd aria-hidden=\"true\">") | |
| 789 | 808 | if templ_7745c5c3_Err != nil { | |
| 790 | 809 | return templ_7745c5c3_Err | |
| 791 | 810 | } | |
| 792 | - | var templ_7745c5c3_Var41 string | |
| 793 | - | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | |
| 811 | + | var templ_7745c5c3_Var42 string | |
| 812 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | |
| 794 | 813 | if templ_7745c5c3_Err != nil { | |
| 795 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 178, Col: 39} | |
| 814 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 39} | |
| 796 | 815 | } | |
| 797 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | |
| 816 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 798 | 817 | if templ_7745c5c3_Err != nil { | |
| 799 | 818 | return templ_7745c5c3_Err | |
| 800 | 819 | } | |
| 801 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</kbd></a></li>") | |
| 820 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</kbd></a></li>") | |
| 802 | 821 | if templ_7745c5c3_Err != nil { | |
| 803 | 822 | return templ_7745c5c3_Err | |
| 804 | 823 | } | |
| 805 | 824 | } | |
| 806 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</ul></nav>") | |
| 825 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</ul></nav>") | |
| 807 | 826 | if templ_7745c5c3_Err != nil { | |
| 808 | 827 | return templ_7745c5c3_Err | |
| 809 | 828 | } | |
| 810 | 829 | } | |
| 811 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</header>") | |
| 830 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</header>") | |
| 812 | 831 | if templ_7745c5c3_Err != nil { | |
| 813 | 832 | return templ_7745c5c3_Err | |
| 814 | 833 | } | |
| @@ -834,38 +853,38 @@ func Footer(l Layout) templ.Component { | |||
| 834 | 853 | }() | |
| 835 | 854 | } | |
| 836 | 855 | ctx = templ.InitializeContext(ctx) | |
| 837 | - | templ_7745c5c3_Var42 := templ.GetChildren(ctx) | |
| 838 | - | if templ_7745c5c3_Var42 == nil { | |
| 839 | - | templ_7745c5c3_Var42 = templ.NopComponent | |
| 856 | + | templ_7745c5c3_Var43 := templ.GetChildren(ctx) | |
| 857 | + | if templ_7745c5c3_Var43 == nil { | |
| 858 | + | templ_7745c5c3_Var43 = templ.NopComponent | |
| 840 | 859 | } | |
| 841 | 860 | ctx = templ.ClearChildren(ctx) | |
| 842 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<footer class=\"foot\"><span>ermite ") | |
| 861 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<footer class=\"foot\"><span>ermite ") | |
| 843 | 862 | if templ_7745c5c3_Err != nil { | |
| 844 | 863 | return templ_7745c5c3_Err | |
| 845 | 864 | } | |
| 846 | - | var templ_7745c5c3_Var43 string | |
| 847 | - | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | |
| 865 | + | var templ_7745c5c3_Var44 string | |
| 866 | + | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | |
| 848 | 867 | if templ_7745c5c3_Err != nil { | |
| 849 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 192, Col: 26} | |
| 868 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 195, Col: 26} | |
| 850 | 869 | } | |
| 851 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 870 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 852 | 871 | if templ_7745c5c3_Err != nil { | |
| 853 | 872 | return templ_7745c5c3_Err | |
| 854 | 873 | } | |
| 855 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</span> <a href=\"") | |
| 874 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</span> <a href=\"") | |
| 856 | 875 | if templ_7745c5c3_Err != nil { | |
| 857 | 876 | return templ_7745c5c3_Err | |
| 858 | 877 | } | |
| 859 | - | var templ_7745c5c3_Var44 templ.SafeURL | |
| 860 | - | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | |
| 878 | + | var templ_7745c5c3_Var45 templ.SafeURL | |
| 879 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | |
| 861 | 880 | if templ_7745c5c3_Err != nil { | |
| 862 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 193, Col: 38} | |
| 881 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 196, Col: 38} | |
| 863 | 882 | } | |
| 864 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 883 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 865 | 884 | if templ_7745c5c3_Err != nil { | |
| 866 | 885 | return templ_7745c5c3_Err | |
| 867 | 886 | } | |
| 868 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>") | |
| 887 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>") | |
| 869 | 888 | if templ_7745c5c3_Err != nil { | |
| 870 | 889 | return templ_7745c5c3_Err | |
| 871 | 890 | } | |
| @@ -893,12 +912,12 @@ func Hint(l Layout) templ.Component { | |||
| 893 | 912 | }() | |
| 894 | 913 | } | |
| 895 | 914 | ctx = templ.InitializeContext(ctx) | |
| 896 | - | templ_7745c5c3_Var45 := templ.GetChildren(ctx) | |
| 897 | - | if templ_7745c5c3_Var45 == nil { | |
| 898 | - | templ_7745c5c3_Var45 = templ.NopComponent | |
| 915 | + | templ_7745c5c3_Var46 := templ.GetChildren(ctx) | |
| 916 | + | if templ_7745c5c3_Var46 == nil { | |
| 917 | + | templ_7745c5c3_Var46 = templ.NopComponent | |
| 899 | 918 | } | |
| 900 | 919 | ctx = templ.ClearChildren(ctx) | |
| 901 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>") | |
| 920 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>") | |
| 902 | 921 | if templ_7745c5c3_Err != nil { | |
| 903 | 922 | return templ_7745c5c3_Err | |
| 904 | 923 | } | |
| @@ -906,7 +925,7 @@ func Hint(l Layout) templ.Component { | |||
| 906 | 925 | if templ_7745c5c3_Err != nil { | |
| 907 | 926 | return templ_7745c5c3_Err | |
| 908 | 927 | } | |
| 909 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</div>") | |
| 928 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</div>") | |
| 910 | 929 | if templ_7745c5c3_Err != nil { | |
| 911 | 930 | return templ_7745c5c3_Err | |
| 912 | 931 | } | |
| @@ -220,15 +220,15 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 220 | if templ_7745c5c3_Err != nil { | 220 | if templ_7745c5c3_Err != nil { | ||
| 221 | return templ_7745c5c3_Err | 221 | return templ_7745c5c3_Err | ||
| 222 | } | 222 | } | ||
| 223 | - | if l.MR { | 223 | + | if l.Lazy { |
| 224 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<script src=\"") | 224 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<script src=\"") | ||
| 225 | if templ_7745c5c3_Err != nil { | 225 | if templ_7745c5c3_Err != nil { | ||
| 226 | return templ_7745c5c3_Err | 226 | return templ_7745c5c3_Err | ||
| 227 | } | 227 | } | ||
| 228 | var templ_7745c5c3_Var16 string | 228 | var templ_7745c5c3_Var16 string | ||
| 229 | - | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.MR) | 229 | + | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Lazy) |
| 230 | if templ_7745c5c3_Err != nil { | 230 | if templ_7745c5c3_Err != nil { | ||
| 231 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 29} | 231 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 31} |
| 232 | } | 232 | } | ||
| 233 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16) | 233 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16) | ||
| 234 | if templ_7745c5c3_Err != nil { | 234 | if templ_7745c5c3_Err != nil { | ||
| @@ -239,15 +239,15 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 239 | return templ_7745c5c3_Err | 239 | return templ_7745c5c3_Err | ||
| 240 | } | 240 | } | ||
| 241 | } | 241 | } | ||
| 242 | - | if l.IssueForm { | 242 | + | if l.MR { |
| 243 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script src=\"") | 243 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script src=\"") | ||
| 244 | if templ_7745c5c3_Err != nil { | 244 | if templ_7745c5c3_Err != nil { | ||
| 245 | return templ_7745c5c3_Err | 245 | return templ_7745c5c3_Err | ||
| 246 | } | 246 | } | ||
| 247 | var templ_7745c5c3_Var17 string | 247 | var templ_7745c5c3_Var17 string | ||
| 248 | - | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.IssueForm) | 248 | + | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.MR) |
| 249 | if templ_7745c5c3_Err != nil { | 249 | if templ_7745c5c3_Err != nil { | ||
| 250 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 37, Col: 36} | 250 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 37, Col: 29} |
| 251 | } | 251 | } | ||
| 252 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) | 252 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) | ||
| 253 | if templ_7745c5c3_Err != nil { | 253 | if templ_7745c5c3_Err != nil { | ||
| @@ -258,69 +258,88 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 258 | return templ_7745c5c3_Err | 258 | return templ_7745c5c3_Err | ||
| 259 | } | 259 | } | ||
| 260 | } | 260 | } | ||
| 261 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</head><body data-ref=\"") | 261 | + | if l.IssueForm { |
| 262 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<script src=\"") | |||
| 263 | + | if templ_7745c5c3_Err != nil { | |||
| 264 | + | return templ_7745c5c3_Err | |||
| 265 | + | } | |||
| 266 | + | var templ_7745c5c3_Var18 string | |||
| 267 | + | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.IssueForm) | |||
| 268 | + | if templ_7745c5c3_Err != nil { | |||
| 269 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 40, Col: 36} | |||
| 270 | + | } | |||
| 271 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |||
| 272 | + | if templ_7745c5c3_Err != nil { | |||
| 273 | + | return templ_7745c5c3_Err | |||
| 274 | + | } | |||
| 275 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" defer></script>") | |||
| 276 | + | if templ_7745c5c3_Err != nil { | |||
| 277 | + | return templ_7745c5c3_Err | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</head><body data-ref=\"") | |||
| 262 | if templ_7745c5c3_Err != nil { | 281 | if templ_7745c5c3_Err != nil { | ||
| 263 | return templ_7745c5c3_Err | 282 | return templ_7745c5c3_Err | ||
| 264 | } | 283 | } | ||
| 265 | - | var templ_7745c5c3_Var18 string | 284 | + | var templ_7745c5c3_Var19 string |
| 266 | - | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath) | 285 | + | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath) |
| 267 | if templ_7745c5c3_Err != nil { | 286 | if templ_7745c5c3_Err != nil { | ||
| 268 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 41, Col: 23} | 287 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 44, Col: 23} |
| 269 | } | 288 | } | ||
| 270 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | 289 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) |
| 271 | if templ_7745c5c3_Err != nil { | 290 | if templ_7745c5c3_Err != nil { | ||
| 272 | return templ_7745c5c3_Err | 291 | return templ_7745c5c3_Err | ||
| 273 | } | 292 | } | ||
| 274 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" data-sha=\"") | 293 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" data-sha=\"") |
| 275 | if templ_7745c5c3_Err != nil { | 294 | if templ_7745c5c3_Err != nil { | ||
| 276 | return templ_7745c5c3_Err | 295 | return templ_7745c5c3_Err | ||
| 277 | } | 296 | } | ||
| 278 | - | var templ_7745c5c3_Var19 string | 297 | + | var templ_7745c5c3_Var20 string |
| 279 | - | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA) | 298 | + | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA) |
| 280 | if templ_7745c5c3_Err != nil { | 299 | if templ_7745c5c3_Err != nil { | ||
| 281 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 42, Col: 19} | 300 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 45, Col: 19} |
| 282 | } | 301 | } | ||
| 283 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) | 302 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) |
| 284 | if templ_7745c5c3_Err != nil { | 303 | if templ_7745c5c3_Err != nil { | ||
| 285 | return templ_7745c5c3_Err | 304 | return templ_7745c5c3_Err | ||
| 286 | } | 305 | } | ||
| 287 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-sel=\"\" data-nav=\"\"") | 306 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-sel=\"\" data-nav=\"\"") |
| 288 | if templ_7745c5c3_Err != nil { | 307 | if templ_7745c5c3_Err != nil { | ||
| 289 | return templ_7745c5c3_Err | 308 | return templ_7745c5c3_Err | ||
| 290 | } | 309 | } | ||
| 291 | if l.PathsURL != "" { | 310 | if l.PathsURL != "" { | ||
| 292 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " data-paths=\"") | 311 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " data-paths=\"") |
| 293 | if templ_7745c5c3_Err != nil { | 312 | if templ_7745c5c3_Err != nil { | ||
| 294 | return templ_7745c5c3_Err | 313 | return templ_7745c5c3_Err | ||
| 295 | } | 314 | } | ||
| 296 | - | var templ_7745c5c3_Var20 string | 315 | + | var templ_7745c5c3_Var21 string |
| 297 | - | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL) | 316 | + | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL) |
| 298 | if templ_7745c5c3_Err != nil { | 317 | if templ_7745c5c3_Err != nil { | ||
| 299 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 46, Col: 27} | 318 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 49, Col: 27} |
| 300 | } | 319 | } | ||
| 301 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) | 320 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) |
| 302 | if templ_7745c5c3_Err != nil { | 321 | if templ_7745c5c3_Err != nil { | ||
| 303 | return templ_7745c5c3_Err | 322 | return templ_7745c5c3_Err | ||
| 304 | } | 323 | } | ||
| 305 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-blob=\"") | 324 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" data-blob=\"") |
| 306 | if templ_7745c5c3_Err != nil { | 325 | if templ_7745c5c3_Err != nil { | ||
| 307 | return templ_7745c5c3_Err | 326 | return templ_7745c5c3_Err | ||
| 308 | } | 327 | } | ||
| 309 | - | var templ_7745c5c3_Var21 string | 328 | + | var templ_7745c5c3_Var22 string |
| 310 | - | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase) | 329 | + | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase) |
| 311 | if templ_7745c5c3_Err != nil { | 330 | if templ_7745c5c3_Err != nil { | ||
| 312 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 47, Col: 26} | 331 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 50, Col: 26} |
| 313 | } | 332 | } | ||
| 314 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) | 333 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22) |
| 315 | if templ_7745c5c3_Err != nil { | 334 | if templ_7745c5c3_Err != nil { | ||
| 316 | return templ_7745c5c3_Err | 335 | return templ_7745c5c3_Err | ||
| 317 | } | 336 | } | ||
| 318 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"") | 337 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"") |
| 319 | if templ_7745c5c3_Err != nil { | 338 | if templ_7745c5c3_Err != nil { | ||
| 320 | return templ_7745c5c3_Err | 339 | return templ_7745c5c3_Err | ||
| 321 | } | 340 | } | ||
| 322 | } | 341 | } | ||
| 323 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "><a class=\"skip\" href=\"#main\">skip to content</a>") | 342 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "><a class=\"skip\" href=\"#main\">skip to content</a>") |
| 324 | if templ_7745c5c3_Err != nil { | 343 | if templ_7745c5c3_Err != nil { | ||
| 325 | return templ_7745c5c3_Err | 344 | return templ_7745c5c3_Err | ||
| 326 | } | 345 | } | ||
| @@ -328,48 +347,48 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 328 | if templ_7745c5c3_Err != nil { | 347 | if templ_7745c5c3_Err != nil { | ||
| 329 | return templ_7745c5c3_Err | 348 | return templ_7745c5c3_Err | ||
| 330 | } | 349 | } | ||
| 331 | - | var templ_7745c5c3_Var22 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)} | 350 | + | var templ_7745c5c3_Var23 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)} |
| 332 | - | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) | 351 | + | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...) |
| 333 | if templ_7745c5c3_Err != nil { | 352 | if templ_7745c5c3_Err != nil { | ||
| 334 | return templ_7745c5c3_Err | 353 | return templ_7745c5c3_Err | ||
| 335 | } | 354 | } | ||
| 336 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"") | 355 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"") |
| 337 | if templ_7745c5c3_Err != nil { | 356 | if templ_7745c5c3_Err != nil { | ||
| 338 | return templ_7745c5c3_Err | 357 | return templ_7745c5c3_Err | ||
| 339 | } | 358 | } | ||
| 340 | - | var templ_7745c5c3_Var23 string | 359 | + | var templ_7745c5c3_Var24 string |
| 341 | - | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var22).String()) | 360 | + | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var23).String()) |
| 342 | if templ_7745c5c3_Err != nil { | 361 | if templ_7745c5c3_Err != nil { | ||
| 343 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0} | 362 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0} | ||
| 344 | } | 363 | } | ||
| 345 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | 364 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) |
| 346 | if templ_7745c5c3_Err != nil { | 365 | if templ_7745c5c3_Err != nil { | ||
| 347 | return templ_7745c5c3_Err | 366 | return templ_7745c5c3_Err | ||
| 348 | } | 367 | } | ||
| 349 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">") | 368 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">") |
| 350 | if templ_7745c5c3_Err != nil { | 369 | if templ_7745c5c3_Err != nil { | ||
| 351 | return templ_7745c5c3_Err | 370 | return templ_7745c5c3_Err | ||
| 352 | } | 371 | } | ||
| 353 | if l.Lede != "" { | 372 | if l.Lede != "" { | ||
| 354 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">") | 373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">") |
| 355 | if templ_7745c5c3_Err != nil { | 374 | if templ_7745c5c3_Err != nil { | ||
| 356 | return templ_7745c5c3_Err | 375 | return templ_7745c5c3_Err | ||
| 357 | } | 376 | } | ||
| 358 | - | var templ_7745c5c3_Var24 string | 377 | + | var templ_7745c5c3_Var25 string |
| 359 | - | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede) | 378 | + | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede) |
| 360 | if templ_7745c5c3_Err != nil { | 379 | if templ_7745c5c3_Err != nil { | ||
| 361 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 54, Col: 81} | 380 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 57, Col: 81} |
| 362 | } | 381 | } | ||
| 363 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) | 382 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) |
| 364 | if templ_7745c5c3_Err != nil { | 383 | if templ_7745c5c3_Err != nil { | ||
| 365 | return templ_7745c5c3_Err | 384 | return templ_7745c5c3_Err | ||
| 366 | } | 385 | } | ||
| 367 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</p></section>") | 386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</p></section>") |
| 368 | if templ_7745c5c3_Err != nil { | 387 | if templ_7745c5c3_Err != nil { | ||
| 369 | return templ_7745c5c3_Err | 388 | return templ_7745c5c3_Err | ||
| 370 | } | 389 | } | ||
| 371 | } | 390 | } | ||
| 372 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<main id=\"main\" tabindex=\"-1\">") | 391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<main id=\"main\" tabindex=\"-1\">") |
| 373 | if templ_7745c5c3_Err != nil { | 392 | if templ_7745c5c3_Err != nil { | ||
| 374 | return templ_7745c5c3_Err | 393 | return templ_7745c5c3_Err | ||
| 375 | } | 394 | } | ||
| @@ -377,12 +396,12 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 377 | if templ_7745c5c3_Err != nil { | 396 | if templ_7745c5c3_Err != nil { | ||
| 378 | return templ_7745c5c3_Err | 397 | return templ_7745c5c3_Err | ||
| 379 | } | 398 | } | ||
| 380 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</main>") | 399 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</main>") |
| 381 | if templ_7745c5c3_Err != nil { | 400 | if templ_7745c5c3_Err != nil { | ||
| 382 | return templ_7745c5c3_Err | 401 | return templ_7745c5c3_Err | ||
| 383 | } | 402 | } | ||
| 384 | if l.HasRail && rail != nil { | 403 | if l.HasRail && rail != nil { | ||
| 385 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<aside class=\"rail\" aria-label=\"Context\">") | 404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<aside class=\"rail\" aria-label=\"Context\">") |
| 386 | if templ_7745c5c3_Err != nil { | 405 | if templ_7745c5c3_Err != nil { | ||
| 387 | return templ_7745c5c3_Err | 406 | return templ_7745c5c3_Err | ||
| 388 | } | 407 | } | ||
| @@ -390,12 +409,12 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 390 | if templ_7745c5c3_Err != nil { | 409 | if templ_7745c5c3_Err != nil { | ||
| 391 | return templ_7745c5c3_Err | 410 | return templ_7745c5c3_Err | ||
| 392 | } | 411 | } | ||
| 393 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</aside>") | 412 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</aside>") |
| 394 | if templ_7745c5c3_Err != nil { | 413 | if templ_7745c5c3_Err != nil { | ||
| 395 | return templ_7745c5c3_Err | 414 | return templ_7745c5c3_Err | ||
| 396 | } | 415 | } | ||
| 397 | } | 416 | } | ||
| 398 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>") | 417 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div>") |
| 399 | if templ_7745c5c3_Err != nil { | 418 | if templ_7745c5c3_Err != nil { | ||
| 400 | return templ_7745c5c3_Err | 419 | return templ_7745c5c3_Err | ||
| 401 | } | 420 | } | ||
| @@ -423,7 +442,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 423 | if templ_7745c5c3_Err != nil { | 442 | if templ_7745c5c3_Err != nil { | ||
| 424 | return templ_7745c5c3_Err | 443 | return templ_7745c5c3_Err | ||
| 425 | } | 444 | } | ||
| 426 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>") | 445 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</body></html>") |
| 427 | if templ_7745c5c3_Err != nil { | 446 | if templ_7745c5c3_Err != nil { | ||
| 428 | return templ_7745c5c3_Err | 447 | return templ_7745c5c3_Err | ||
| 429 | } | 448 | } | ||
| @@ -449,366 +468,366 @@ func Header(l Layout) templ.Component { | |||||
| 449 | }() | 468 | }() | ||
| 450 | } | 469 | } | ||
| 451 | ctx = templ.InitializeContext(ctx) | 470 | ctx = templ.InitializeContext(ctx) | ||
| 452 | - | templ_7745c5c3_Var25 := templ.GetChildren(ctx) | 471 | + | templ_7745c5c3_Var26 := templ.GetChildren(ctx) |
| 453 | - | if templ_7745c5c3_Var25 == nil { | 472 | + | if templ_7745c5c3_Var26 == nil { |
| 454 | - | templ_7745c5c3_Var25 = templ.NopComponent | 473 | + | templ_7745c5c3_Var26 = templ.NopComponent |
| 455 | } | 474 | } | ||
| 456 | ctx = templ.ClearChildren(ctx) | 475 | ctx = templ.ClearChildren(ctx) | ||
| 457 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ") | 476 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ") |
| 458 | if templ_7745c5c3_Err != nil { | 477 | if templ_7745c5c3_Err != nil { | ||
| 459 | return templ_7745c5c3_Err | 478 | return templ_7745c5c3_Err | ||
| 460 | } | 479 | } | ||
| 461 | if l.Inbox != nil { | 480 | if l.Inbox != nil { | ||
| 462 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a class=\"btn inbox-btn\" href=\"") | 481 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a class=\"btn inbox-btn\" href=\"") |
| 463 | if templ_7745c5c3_Err != nil { | 482 | if templ_7745c5c3_Err != nil { | ||
| 464 | return templ_7745c5c3_Err | 483 | return templ_7745c5c3_Err | ||
| 465 | } | 484 | } | ||
| 466 | - | var templ_7745c5c3_Var26 templ.SafeURL | 485 | + | var templ_7745c5c3_Var27 templ.SafeURL |
| 467 | - | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href)) | 486 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href)) |
| 468 | if templ_7745c5c3_Err != nil { | 487 | if templ_7745c5c3_Err != nil { | ||
| 469 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 86, Col: 39} | 488 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 89, Col: 39} |
| 470 | } | 489 | } | ||
| 471 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | 490 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) |
| 472 | if templ_7745c5c3_Err != nil { | 491 | if templ_7745c5c3_Err != nil { | ||
| 473 | return templ_7745c5c3_Err | 492 | return templ_7745c5c3_Err | ||
| 474 | } | 493 | } | ||
| 475 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ") | 494 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ") |
| 476 | if templ_7745c5c3_Err != nil { | 495 | if templ_7745c5c3_Err != nil { | ||
| 477 | return templ_7745c5c3_Err | 496 | return templ_7745c5c3_Err | ||
| 478 | } | 497 | } | ||
| 479 | if l.Inbox.Count >= 0 { | 498 | if l.Inbox.Count >= 0 { | ||
| 480 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<span class=\"count\">") | 499 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<span class=\"count\">") |
| 481 | if templ_7745c5c3_Err != nil { | 500 | if templ_7745c5c3_Err != nil { | ||
| 482 | return templ_7745c5c3_Err | 501 | return templ_7745c5c3_Err | ||
| 483 | } | 502 | } | ||
| 484 | - | var templ_7745c5c3_Var27 string | 503 | + | var templ_7745c5c3_Var28 string |
| 485 | - | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count)) | 504 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count)) |
| 486 | if templ_7745c5c3_Err != nil { | 505 | if templ_7745c5c3_Err != nil { | ||
| 487 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 92, Col: 47} | 506 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 47} |
| 488 | } | 507 | } | ||
| 489 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | 508 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) |
| 490 | if templ_7745c5c3_Err != nil { | 509 | if templ_7745c5c3_Err != nil { | ||
| 491 | return templ_7745c5c3_Err | 510 | return templ_7745c5c3_Err | ||
| 492 | } | 511 | } | ||
| 493 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span>") | 512 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</span>") |
| 494 | if templ_7745c5c3_Err != nil { | 513 | if templ_7745c5c3_Err != nil { | ||
| 495 | return templ_7745c5c3_Err | 514 | return templ_7745c5c3_Err | ||
| 496 | } | 515 | } | ||
| 497 | } | 516 | } | ||
| 498 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a> ") | 517 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a> ") |
| 499 | if templ_7745c5c3_Err != nil { | 518 | if templ_7745c5c3_Err != nil { | ||
| 500 | return templ_7745c5c3_Err | 519 | return templ_7745c5c3_Err | ||
| 501 | } | 520 | } | ||
| 502 | } | 521 | } | ||
| 503 | if len(l.Crumbs) > 0 { | 522 | if len(l.Crumbs) > 0 { | ||
| 504 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>") | 523 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>") |
| 505 | if templ_7745c5c3_Err != nil { | 524 | if templ_7745c5c3_Err != nil { | ||
| 506 | return templ_7745c5c3_Err | 525 | return templ_7745c5c3_Err | ||
| 507 | } | 526 | } | ||
| 508 | for _, c := range l.Crumbs { | 527 | for _, c := range l.Crumbs { | ||
| 509 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<li>") | 528 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<li>") |
| 510 | if templ_7745c5c3_Err != nil { | 529 | if templ_7745c5c3_Err != nil { | ||
| 511 | return templ_7745c5c3_Err | 530 | return templ_7745c5c3_Err | ||
| 512 | } | 531 | } | ||
| 513 | if c.Href != "" { | 532 | if c.Href != "" { | ||
| 514 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a href=\"") | 533 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<a href=\"") |
| 515 | if templ_7745c5c3_Err != nil { | 534 | if templ_7745c5c3_Err != nil { | ||
| 516 | return templ_7745c5c3_Err | 535 | return templ_7745c5c3_Err | ||
| 517 | } | 536 | } | ||
| 518 | - | var templ_7745c5c3_Var28 templ.SafeURL | 537 | + | var templ_7745c5c3_Var29 templ.SafeURL |
| 519 | - | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | 538 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) |
| 520 | if templ_7745c5c3_Err != nil { | 539 | if templ_7745c5c3_Err != nil { | ||
| 521 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 103, Col: 38} | 540 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 38} |
| 522 | } | 541 | } | ||
| 523 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | 542 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) |
| 524 | if templ_7745c5c3_Err != nil { | 543 | if templ_7745c5c3_Err != nil { | ||
| 525 | return templ_7745c5c3_Err | 544 | return templ_7745c5c3_Err | ||
| 526 | } | 545 | } | ||
| 527 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"") | 546 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"") |
| 528 | if templ_7745c5c3_Err != nil { | 547 | if templ_7745c5c3_Err != nil { | ||
| 529 | return templ_7745c5c3_Err | 548 | return templ_7745c5c3_Err | ||
| 530 | } | 549 | } | ||
| 531 | if c.Current { | 550 | if c.Current { | ||
| 532 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " aria-current=\"page\"") | 551 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " aria-current=\"page\"") |
| 533 | if templ_7745c5c3_Err != nil { | 552 | if templ_7745c5c3_Err != nil { | ||
| 534 | return templ_7745c5c3_Err | 553 | return templ_7745c5c3_Err | ||
| 535 | } | 554 | } | ||
| 536 | } | 555 | } | ||
| 537 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, ">") | 556 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">") |
| 538 | if templ_7745c5c3_Err != nil { | 557 | if templ_7745c5c3_Err != nil { | ||
| 539 | return templ_7745c5c3_Err | 558 | return templ_7745c5c3_Err | ||
| 540 | } | 559 | } | ||
| 541 | - | var templ_7745c5c3_Var29 string | 560 | + | var templ_7745c5c3_Var30 string |
| 542 | - | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | 561 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) |
| 543 | if templ_7745c5c3_Err != nil { | 562 | if templ_7745c5c3_Err != nil { | ||
| 544 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 107, Col: 19} | 563 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 19} |
| 545 | } | 564 | } | ||
| 546 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | 565 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) |
| 547 | if templ_7745c5c3_Err != nil { | 566 | if templ_7745c5c3_Err != nil { | ||
| 548 | return templ_7745c5c3_Err | 567 | return templ_7745c5c3_Err | ||
| 549 | } | 568 | } | ||
| 550 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</a>") | 569 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</a>") |
| 551 | if templ_7745c5c3_Err != nil { | 570 | if templ_7745c5c3_Err != nil { | ||
| 552 | return templ_7745c5c3_Err | 571 | return templ_7745c5c3_Err | ||
| 553 | } | 572 | } | ||
| 554 | } else { | 573 | } else { | ||
| 555 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<span") | 574 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span") |
| 556 | if templ_7745c5c3_Err != nil { | 575 | if templ_7745c5c3_Err != nil { | ||
| 557 | return templ_7745c5c3_Err | 576 | return templ_7745c5c3_Err | ||
| 558 | } | 577 | } | ||
| 559 | if c.Current { | 578 | if c.Current { | ||
| 560 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " aria-current=\"page\"") | 579 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " aria-current=\"page\"") |
| 561 | if templ_7745c5c3_Err != nil { | 580 | if templ_7745c5c3_Err != nil { | ||
| 562 | return templ_7745c5c3_Err | 581 | return templ_7745c5c3_Err | ||
| 563 | } | 582 | } | ||
| 564 | } | 583 | } | ||
| 565 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">") | 584 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, ">") |
| 566 | if templ_7745c5c3_Err != nil { | 585 | if templ_7745c5c3_Err != nil { | ||
| 567 | return templ_7745c5c3_Err | 586 | return templ_7745c5c3_Err | ||
| 568 | } | 587 | } | ||
| 569 | - | var templ_7745c5c3_Var30 string | 588 | + | var templ_7745c5c3_Var31 string |
| 570 | - | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | 589 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) |
| 571 | if templ_7745c5c3_Err != nil { | 590 | if templ_7745c5c3_Err != nil { | ||
| 572 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 113, Col: 19} | 591 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 116, Col: 19} |
| 573 | } | 592 | } | ||
| 574 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | 593 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) |
| 575 | if templ_7745c5c3_Err != nil { | 594 | if templ_7745c5c3_Err != nil { | ||
| 576 | return templ_7745c5c3_Err | 595 | return templ_7745c5c3_Err | ||
| 577 | } | 596 | } | ||
| 578 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span>") | 597 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span>") |
| 579 | if templ_7745c5c3_Err != nil { | 598 | if templ_7745c5c3_Err != nil { | ||
| 580 | return templ_7745c5c3_Err | 599 | return templ_7745c5c3_Err | ||
| 581 | } | 600 | } | ||
| 582 | } | 601 | } | ||
| 583 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</li>") | 602 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</li>") |
| 584 | if templ_7745c5c3_Err != nil { | 603 | if templ_7745c5c3_Err != nil { | ||
| 585 | return templ_7745c5c3_Err | 604 | return templ_7745c5c3_Err | ||
| 586 | } | 605 | } | ||
| 587 | } | 606 | } | ||
| 588 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</ol></nav>") | 607 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</ol></nav>") |
| 589 | if templ_7745c5c3_Err != nil { | 608 | if templ_7745c5c3_Err != nil { | ||
| 590 | return templ_7745c5c3_Err | 609 | return templ_7745c5c3_Err | ||
| 591 | } | 610 | } | ||
| 592 | } | 611 | } | ||
| 593 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"hdr-acts\">") | 612 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"hdr-acts\">") |
| 594 | if templ_7745c5c3_Err != nil { | 613 | if templ_7745c5c3_Err != nil { | ||
| 595 | return templ_7745c5c3_Err | 614 | return templ_7745c5c3_Err | ||
| 596 | } | 615 | } | ||
| 597 | if l.Ref != "" { | 616 | if l.Ref != "" { | ||
| 598 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a class=\"btn ref\" href=\"") | 617 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn ref\" href=\"") |
| 599 | if templ_7745c5c3_Err != nil { | 618 | if templ_7745c5c3_Err != nil { | ||
| 600 | return templ_7745c5c3_Err | 619 | return templ_7745c5c3_Err | ||
| 601 | } | 620 | } | ||
| 602 | - | var templ_7745c5c3_Var31 templ.SafeURL | 621 | + | var templ_7745c5c3_Var32 templ.SafeURL |
| 603 | - | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs"))) | 622 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs"))) |
| 604 | if templ_7745c5c3_Err != nil { | 623 | if templ_7745c5c3_Err != nil { | ||
| 605 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 124, Col: 52} | 624 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 52} |
| 606 | } | 625 | } | ||
| 607 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | 626 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) |
| 608 | if templ_7745c5c3_Err != nil { | 627 | if templ_7745c5c3_Err != nil { | ||
| 609 | return templ_7745c5c3_Err | 628 | return templ_7745c5c3_Err | ||
| 610 | } | 629 | } | ||
| 611 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">") | 630 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">") |
| 612 | if templ_7745c5c3_Err != nil { | 631 | if templ_7745c5c3_Err != nil { | ||
| 613 | return templ_7745c5c3_Err | 632 | return templ_7745c5c3_Err | ||
| 614 | } | 633 | } | ||
| 615 | - | var templ_7745c5c3_Var32 string | 634 | + | var templ_7745c5c3_Var33 string |
| 616 | - | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref) | 635 | + | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref) |
| 617 | if templ_7745c5c3_Err != nil { | 636 | if templ_7745c5c3_Err != nil { | ||
| 618 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 36} | 637 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 36} |
| 619 | } | 638 | } | ||
| 620 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | 639 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) |
| 621 | if templ_7745c5c3_Err != nil { | 640 | if templ_7745c5c3_Err != nil { | ||
| 622 | return templ_7745c5c3_Err | 641 | return templ_7745c5c3_Err | ||
| 623 | } | 642 | } | ||
| 624 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span> <span class=\"sha\">") | 643 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</span> <span class=\"sha\">") |
| 625 | if templ_7745c5c3_Err != nil { | 644 | if templ_7745c5c3_Err != nil { | ||
| 626 | return templ_7745c5c3_Err | 645 | return templ_7745c5c3_Err | ||
| 627 | } | 646 | } | ||
| 628 | - | var templ_7745c5c3_Var33 string | 647 | + | var templ_7745c5c3_Var34 string |
| 629 | - | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short) | 648 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short) |
| 630 | if templ_7745c5c3_Err != nil { | 649 | if templ_7745c5c3_Err != nil { | ||
| 631 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 131, Col: 33} | 650 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 33} |
| 632 | } | 651 | } | ||
| 633 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | 652 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) |
| 634 | if templ_7745c5c3_Err != nil { | 653 | if templ_7745c5c3_Err != nil { | ||
| 635 | return templ_7745c5c3_Err | 654 | return templ_7745c5c3_Err | ||
| 636 | } | 655 | } | ||
| 637 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></a> ") | 656 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span></a> ") |
| 638 | if templ_7745c5c3_Err != nil { | 657 | if templ_7745c5c3_Err != nil { | ||
| 639 | return templ_7745c5c3_Err | 658 | return templ_7745c5c3_Err | ||
| 640 | } | 659 | } | ||
| 641 | } | 660 | } | ||
| 642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ") | 661 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ") |
| 643 | if templ_7745c5c3_Err != nil { | 662 | if templ_7745c5c3_Err != nil { | ||
| 644 | return templ_7745c5c3_Err | 663 | return templ_7745c5c3_Err | ||
| 645 | } | 664 | } | ||
| 646 | if SignedIn(ctx) { | 665 | if SignedIn(ctx) { | ||
| 647 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>") | 666 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>") |
| 648 | if templ_7745c5c3_Err != nil { | 667 | if templ_7745c5c3_Err != nil { | ||
| 649 | return templ_7745c5c3_Err | 668 | return templ_7745c5c3_Err | ||
| 650 | } | 669 | } | ||
| 651 | - | var templ_7745c5c3_Var34 string | 670 | + | var templ_7745c5c3_Var35 string |
| 652 | - | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner) | 671 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner) |
| 653 | if templ_7745c5c3_Err != nil { | 672 | if templ_7745c5c3_Err != nil { | ||
| 654 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 123} | 673 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 123} |
| 655 | } | 674 | } | ||
| 656 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | 675 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) |
| 657 | if templ_7745c5c3_Err != nil { | 676 | if templ_7745c5c3_Err != nil { | ||
| 658 | return templ_7745c5c3_Err | 677 | return templ_7745c5c3_Err | ||
| 659 | } | 678 | } | ||
| 660 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> ") | 679 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</span> ") |
| 661 | if templ_7745c5c3_Err != nil { | 680 | if templ_7745c5c3_Err != nil { | ||
| 662 | return templ_7745c5c3_Err | 681 | return templ_7745c5c3_Err | ||
| 663 | } | 682 | } | ||
| 664 | if l.Passkeys { | 683 | if l.Passkeys { | ||
| 665 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ") | 684 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ") |
| 666 | if templ_7745c5c3_Err != nil { | 685 | if templ_7745c5c3_Err != nil { | ||
| 667 | return templ_7745c5c3_Err | 686 | return templ_7745c5c3_Err | ||
| 668 | } | 687 | } | ||
| 669 | } | 688 | } | ||
| 670 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>") | 689 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>") |
| 671 | if templ_7745c5c3_Err != nil { | 690 | if templ_7745c5c3_Err != nil { | ||
| 672 | return templ_7745c5c3_Err | 691 | return templ_7745c5c3_Err | ||
| 673 | } | 692 | } | ||
| 674 | } else if l.Passkeys { | 693 | } else if l.Passkeys { | ||
| 675 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>") | 694 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>") |
| 676 | if templ_7745c5c3_Err != nil { | 695 | if templ_7745c5c3_Err != nil { | ||
| 677 | return templ_7745c5c3_Err | 696 | return templ_7745c5c3_Err | ||
| 678 | } | 697 | } | ||
| 679 | } | 698 | } | ||
| 680 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div></div>") | 699 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</div></div>") |
| 681 | if templ_7745c5c3_Err != nil { | 700 | if templ_7745c5c3_Err != nil { | ||
| 682 | return templ_7745c5c3_Err | 701 | return templ_7745c5c3_Err | ||
| 683 | } | 702 | } | ||
| 684 | if len(l.Sections) > 0 { | 703 | if len(l.Sections) > 0 { | ||
| 685 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>") | 704 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>") |
| 686 | if templ_7745c5c3_Err != nil { | 705 | if templ_7745c5c3_Err != nil { | ||
| 687 | return templ_7745c5c3_Err | 706 | return templ_7745c5c3_Err | ||
| 688 | } | 707 | } | ||
| 689 | for _, s := range l.Sections { | 708 | for _, s := range l.Sections { | ||
| 690 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<li><a href=\"") | 709 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<li><a href=\"") |
| 691 | if templ_7745c5c3_Err != nil { | 710 | if templ_7745c5c3_Err != nil { | ||
| 692 | return templ_7745c5c3_Err | 711 | return templ_7745c5c3_Err | ||
| 693 | } | 712 | } | ||
| 694 | - | var templ_7745c5c3_Var35 templ.SafeURL | 713 | + | var templ_7745c5c3_Var36 templ.SafeURL |
| 695 | - | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | 714 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) |
| 696 | if templ_7745c5c3_Err != nil { | 715 | if templ_7745c5c3_Err != nil { | ||
| 697 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 36} | 716 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 170, Col: 36} |
| 698 | } | 717 | } | ||
| 699 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | 718 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) |
| 700 | if templ_7745c5c3_Err != nil { | 719 | if templ_7745c5c3_Err != nil { | ||
| 701 | return templ_7745c5c3_Err | 720 | return templ_7745c5c3_Err | ||
| 702 | } | 721 | } | ||
| 703 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" data-key=\"") | 722 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" data-key=\"") |
| 704 | if templ_7745c5c3_Err != nil { | 723 | if templ_7745c5c3_Err != nil { | ||
| 705 | return templ_7745c5c3_Err | 724 | return templ_7745c5c3_Err | ||
| 706 | } | 725 | } | ||
| 707 | - | var templ_7745c5c3_Var36 string | 726 | + | var templ_7745c5c3_Var37 string |
| 708 | - | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | 727 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) |
| 709 | if templ_7745c5c3_Err != nil { | 728 | if templ_7745c5c3_Err != nil { | ||
| 710 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 168, Col: 24} | 729 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 171, Col: 24} |
| 711 | } | 730 | } | ||
| 712 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) | 731 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) |
| 713 | if templ_7745c5c3_Err != nil { | 732 | if templ_7745c5c3_Err != nil { | ||
| 714 | return templ_7745c5c3_Err | 733 | return templ_7745c5c3_Err | ||
| 715 | } | 734 | } | ||
| 716 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" aria-keyshortcuts=\"") | 735 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" aria-keyshortcuts=\"") |
| 717 | if templ_7745c5c3_Err != nil { | 736 | if templ_7745c5c3_Err != nil { | ||
| 718 | return templ_7745c5c3_Err | 737 | return templ_7745c5c3_Err | ||
| 719 | } | 738 | } | ||
| 720 | - | var templ_7745c5c3_Var37 string | 739 | + | var templ_7745c5c3_Var38 string |
| 721 | - | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | 740 | + | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) |
| 722 | if templ_7745c5c3_Err != nil { | 741 | if templ_7745c5c3_Err != nil { | ||
| 723 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 169, Col: 33} | 742 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 172, Col: 33} |
| 724 | } | 743 | } | ||
| 725 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | 744 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38) |
| 726 | if templ_7745c5c3_Err != nil { | 745 | if templ_7745c5c3_Err != nil { | ||
| 727 | return templ_7745c5c3_Err | 746 | return templ_7745c5c3_Err | ||
| 728 | } | 747 | } | ||
| 729 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\"") | 748 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\"") |
| 730 | if templ_7745c5c3_Err != nil { | 749 | if templ_7745c5c3_Err != nil { | ||
| 731 | return templ_7745c5c3_Err | 750 | return templ_7745c5c3_Err | ||
| 732 | } | 751 | } | ||
| 733 | if s.Current { | 752 | if s.Current { | ||
| 734 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " aria-current=\"page\"") | 753 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " aria-current=\"page\"") |
| 735 | if templ_7745c5c3_Err != nil { | 754 | if templ_7745c5c3_Err != nil { | ||
| 736 | return templ_7745c5c3_Err | 755 | return templ_7745c5c3_Err | ||
| 737 | } | 756 | } | ||
| 738 | } | 757 | } | ||
| 739 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, ">") | 758 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, ">") |
| 740 | if templ_7745c5c3_Err != nil { | 759 | if templ_7745c5c3_Err != nil { | ||
| 741 | return templ_7745c5c3_Err | 760 | return templ_7745c5c3_Err | ||
| 742 | } | 761 | } | ||
| 743 | - | var templ_7745c5c3_Var38 string | 762 | + | var templ_7745c5c3_Var39 string |
| 744 | - | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | 763 | + | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) |
| 745 | if templ_7745c5c3_Err != nil { | 764 | if templ_7745c5c3_Err != nil { | ||
| 746 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 174, Col: 17} | 765 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 177, Col: 17} |
| 747 | } | 766 | } | ||
| 748 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) | 767 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) |
| 749 | if templ_7745c5c3_Err != nil { | 768 | if templ_7745c5c3_Err != nil { | ||
| 750 | return templ_7745c5c3_Err | 769 | return templ_7745c5c3_Err | ||
| 751 | } | 770 | } | ||
| 752 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ") | 771 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ") |
| 753 | if templ_7745c5c3_Err != nil { | 772 | if templ_7745c5c3_Err != nil { | ||
| 754 | return templ_7745c5c3_Err | 773 | return templ_7745c5c3_Err | ||
| 755 | } | 774 | } | ||
| 756 | if s.Count != "" { | 775 | if s.Count != "" { | ||
| 757 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<span class=\"count\">") | 776 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<span class=\"count\">") |
| 758 | if templ_7745c5c3_Err != nil { | 777 | if templ_7745c5c3_Err != nil { | ||
| 759 | return templ_7745c5c3_Err | 778 | return templ_7745c5c3_Err | ||
| 760 | } | 779 | } | ||
| 761 | - | var templ_7745c5c3_Var39 string | 780 | + | var templ_7745c5c3_Var40 string |
| 762 | - | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count) | 781 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count) |
| 763 | if templ_7745c5c3_Err != nil { | 782 | if templ_7745c5c3_Err != nil { | ||
| 764 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 38} | 783 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 38} |
| 765 | } | 784 | } | ||
| 766 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | 785 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) |
| 767 | if templ_7745c5c3_Err != nil { | 786 | if templ_7745c5c3_Err != nil { | ||
| 768 | return templ_7745c5c3_Err | 787 | return templ_7745c5c3_Err | ||
| 769 | } | 788 | } | ||
| 770 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<span class=\"sr\">") | 789 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<span class=\"sr\">") |
| 771 | if templ_7745c5c3_Err != nil { | 790 | if templ_7745c5c3_Err != nil { | ||
| 772 | return templ_7745c5c3_Err | 791 | return templ_7745c5c3_Err | ||
| 773 | } | 792 | } | ||
| 774 | - | var templ_7745c5c3_Var40 string | 793 | + | var templ_7745c5c3_Var41 string |
| 775 | - | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(" open") | 794 | + | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(" open") |
| 776 | if templ_7745c5c3_Err != nil { | 795 | if templ_7745c5c3_Err != nil { | ||
| 777 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 66} | 796 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 66} |
| 778 | } | 797 | } | ||
| 779 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) | 798 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) |
| 780 | if templ_7745c5c3_Err != nil { | 799 | if templ_7745c5c3_Err != nil { | ||
| 781 | return templ_7745c5c3_Err | 800 | return templ_7745c5c3_Err | ||
| 782 | } | 801 | } | ||
| 783 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</span></span> ") | 802 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</span></span> ") |
| 784 | if templ_7745c5c3_Err != nil { | 803 | if templ_7745c5c3_Err != nil { | ||
| 785 | return templ_7745c5c3_Err | 804 | return templ_7745c5c3_Err | ||
| 786 | } | 805 | } | ||
| 787 | } | 806 | } | ||
| 788 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<kbd aria-hidden=\"true\">") | 807 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<kbd aria-hidden=\"true\">") |
| 789 | if templ_7745c5c3_Err != nil { | 808 | if templ_7745c5c3_Err != nil { | ||
| 790 | return templ_7745c5c3_Err | 809 | return templ_7745c5c3_Err | ||
| 791 | } | 810 | } | ||
| 792 | - | var templ_7745c5c3_Var41 string | 811 | + | var templ_7745c5c3_Var42 string |
| 793 | - | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | 812 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) |
| 794 | if templ_7745c5c3_Err != nil { | 813 | if templ_7745c5c3_Err != nil { | ||
| 795 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 178, Col: 39} | 814 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 39} |
| 796 | } | 815 | } | ||
| 797 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | 816 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) |
| 798 | if templ_7745c5c3_Err != nil { | 817 | if templ_7745c5c3_Err != nil { | ||
| 799 | return templ_7745c5c3_Err | 818 | return templ_7745c5c3_Err | ||
| 800 | } | 819 | } | ||
| 801 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</kbd></a></li>") | 820 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</kbd></a></li>") |
| 802 | if templ_7745c5c3_Err != nil { | 821 | if templ_7745c5c3_Err != nil { | ||
| 803 | return templ_7745c5c3_Err | 822 | return templ_7745c5c3_Err | ||
| 804 | } | 823 | } | ||
| 805 | } | 824 | } | ||
| 806 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</ul></nav>") | 825 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</ul></nav>") |
| 807 | if templ_7745c5c3_Err != nil { | 826 | if templ_7745c5c3_Err != nil { | ||
| 808 | return templ_7745c5c3_Err | 827 | return templ_7745c5c3_Err | ||
| 809 | } | 828 | } | ||
| 810 | } | 829 | } | ||
| 811 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</header>") | 830 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</header>") |
| 812 | if templ_7745c5c3_Err != nil { | 831 | if templ_7745c5c3_Err != nil { | ||
| 813 | return templ_7745c5c3_Err | 832 | return templ_7745c5c3_Err | ||
| 814 | } | 833 | } | ||
| @@ -834,38 +853,38 @@ func Footer(l Layout) templ.Component { | |||||
| 834 | }() | 853 | }() | ||
| 835 | } | 854 | } | ||
| 836 | ctx = templ.InitializeContext(ctx) | 855 | ctx = templ.InitializeContext(ctx) | ||
| 837 | - | templ_7745c5c3_Var42 := templ.GetChildren(ctx) | 856 | + | templ_7745c5c3_Var43 := templ.GetChildren(ctx) |
| 838 | - | if templ_7745c5c3_Var42 == nil { | 857 | + | if templ_7745c5c3_Var43 == nil { |
| 839 | - | templ_7745c5c3_Var42 = templ.NopComponent | 858 | + | templ_7745c5c3_Var43 = templ.NopComponent |
| 840 | } | 859 | } | ||
| 841 | ctx = templ.ClearChildren(ctx) | 860 | ctx = templ.ClearChildren(ctx) | ||
| 842 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<footer class=\"foot\"><span>ermite ") | 861 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<footer class=\"foot\"><span>ermite ") |
| 843 | if templ_7745c5c3_Err != nil { | 862 | if templ_7745c5c3_Err != nil { | ||
| 844 | return templ_7745c5c3_Err | 863 | return templ_7745c5c3_Err | ||
| 845 | } | 864 | } | ||
| 846 | - | var templ_7745c5c3_Var43 string | 865 | + | var templ_7745c5c3_Var44 string |
| 847 | - | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | 866 | + | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) |
| 848 | if templ_7745c5c3_Err != nil { | 867 | if templ_7745c5c3_Err != nil { | ||
| 849 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 192, Col: 26} | 868 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 195, Col: 26} |
| 850 | } | 869 | } | ||
| 851 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | 870 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) |
| 852 | if templ_7745c5c3_Err != nil { | 871 | if templ_7745c5c3_Err != nil { | ||
| 853 | return templ_7745c5c3_Err | 872 | return templ_7745c5c3_Err | ||
| 854 | } | 873 | } | ||
| 855 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</span> <a href=\"") | 874 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</span> <a href=\"") |
| 856 | if templ_7745c5c3_Err != nil { | 875 | if templ_7745c5c3_Err != nil { | ||
| 857 | return templ_7745c5c3_Err | 876 | return templ_7745c5c3_Err | ||
| 858 | } | 877 | } | ||
| 859 | - | var templ_7745c5c3_Var44 templ.SafeURL | 878 | + | var templ_7745c5c3_Var45 templ.SafeURL |
| 860 | - | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | 879 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) |
| 861 | if templ_7745c5c3_Err != nil { | 880 | if templ_7745c5c3_Err != nil { | ||
| 862 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 193, Col: 38} | 881 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 196, Col: 38} |
| 863 | } | 882 | } | ||
| 864 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | 883 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) |
| 865 | if templ_7745c5c3_Err != nil { | 884 | if templ_7745c5c3_Err != nil { | ||
| 866 | return templ_7745c5c3_Err | 885 | return templ_7745c5c3_Err | ||
| 867 | } | 886 | } | ||
| 868 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>") | 887 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>") |
| 869 | if templ_7745c5c3_Err != nil { | 888 | if templ_7745c5c3_Err != nil { | ||
| 870 | return templ_7745c5c3_Err | 889 | return templ_7745c5c3_Err | ||
| 871 | } | 890 | } | ||
| @@ -893,12 +912,12 @@ func Hint(l Layout) templ.Component { | |||||
| 893 | }() | 912 | }() | ||
| 894 | } | 913 | } | ||
| 895 | ctx = templ.InitializeContext(ctx) | 914 | ctx = templ.InitializeContext(ctx) | ||
| 896 | - | templ_7745c5c3_Var45 := templ.GetChildren(ctx) | 915 | + | templ_7745c5c3_Var46 := templ.GetChildren(ctx) |
| 897 | - | if templ_7745c5c3_Var45 == nil { | 916 | + | if templ_7745c5c3_Var46 == nil { |
| 898 | - | templ_7745c5c3_Var45 = templ.NopComponent | 917 | + | templ_7745c5c3_Var46 = templ.NopComponent |
| 899 | } | 918 | } | ||
| 900 | ctx = templ.ClearChildren(ctx) | 919 | ctx = templ.ClearChildren(ctx) | ||
| 901 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>") | 920 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>") |
| 902 | if templ_7745c5c3_Err != nil { | 921 | if templ_7745c5c3_Err != nil { | ||
| 903 | return templ_7745c5c3_Err | 922 | return templ_7745c5c3_Err | ||
| 904 | } | 923 | } | ||
| @@ -906,7 +925,7 @@ func Hint(l Layout) templ.Component { | |||||
| 906 | if templ_7745c5c3_Err != nil { | 925 | if templ_7745c5c3_Err != nil { | ||
| 907 | return templ_7745c5c3_Err | 926 | return templ_7745c5c3_Err | ||
| 908 | } | 927 | } | ||
| 909 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</div>") | 928 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</div>") |
| 910 | if templ_7745c5c3_Err != nil { | 929 | if templ_7745c5c3_Err != nil { | ||
| 911 | return templ_7745c5c3_Err | 930 | return templ_7745c5c3_Err | ||
| 912 | } | 931 | } | ||
M internal/adapters/web/static/css/60-mr.css +3 -1
| @@ -191,7 +191,9 @@ | |||
| 191 | 191 | &.dlazy-1 { height: 120px; } &.dlazy-2 { height: 360px; } &.dlazy-3 { height: 900px; } &.dlazy-4 { height: 1600px; } | |
| 192 | 192 | /* with scripts the window comes by itself: the link of the page without | |
| 193 | 193 | scripts would only flash */ | |
| 194 | - | html.js & > a { visibility: hidden; } | |
| 194 | + | html.js &:not([data-manual]) > a { visibility: hidden; } | |
| 195 | + | &[data-manual] { height: auto; padding: var(--s3); } | |
| 196 | + | &[data-manual][data-loading] > a { opacity: .5; } | |
| 195 | 197 | & .notice { margin: 0; } | |
| 196 | 198 | } | |
| 197 | 199 | .dloose { | |
| @@ -191,7 +191,9 @@ | |||||
| 191 | &.dlazy-1 { height: 120px; } &.dlazy-2 { height: 360px; } &.dlazy-3 { height: 900px; } &.dlazy-4 { height: 1600px; } | 191 | &.dlazy-1 { height: 120px; } &.dlazy-2 { height: 360px; } &.dlazy-3 { height: 900px; } &.dlazy-4 { height: 1600px; } | ||
| 192 | /* with scripts the window comes by itself: the link of the page without | 192 | /* with scripts the window comes by itself: the link of the page without | ||
| 193 | scripts would only flash */ | 193 | scripts would only flash */ | ||
| 194 | - | html.js & > a { visibility: hidden; } | 194 | + | html.js &:not([data-manual]) > a { visibility: hidden; } |
| 195 | + | &[data-manual] { height: auto; padding: var(--s3); } | |||
| 196 | + | &[data-manual][data-loading] > a { opacity: .5; } | |||
| 195 | & .notice { margin: 0; } | 197 | & .notice { margin: 0; } | ||
| 196 | } | 198 | } | ||
| 197 | .dloose { | 199 | .dloose { | ||
A internal/adapters/web/static/js/lazy.js +166 -0
| @@ -0,0 +1,166 @@ | |||
| 1 | + | // lazy.js: the files of a diff that are not in the page yet (commit, compare | |
| 2 | + | // and merge request pages). | |
| 3 | + | // | |
| 4 | + | // A placeholder is a .dlazy[data-src] in a details.dfile. Two kinds: | |
| 5 | + | // - a window of a merge request: loaded by itself when it comes within two | |
| 6 | + | // screens of the view (the files of its window replace the placeholders of | |
| 7 | + | // the same ids); | |
| 8 | + | // - a collapsed file (generated, large) [data-manual]: never loaded by | |
| 9 | + | // itself. Its "load diff" link loads that one file (click, or Enter / Space | |
| 10 | + | // on the link); without scripts the link reloads the page with the file. | |
| 11 | + | // A fragment is fetched once. Nothing observes the DOM: the one | |
| 12 | + | // IntersectionObserver watches the automatic placeholders, and a manual one | |
| 13 | + | // costs no more than its markup. | |
| 14 | + | // | |
| 15 | + | // window.ermiteLazy.load is also the loader of mr.js (reveal). | |
| 16 | + | window.ermiteLazy = (() => { | |
| 17 | + | const $ = (sel, root = document) => root.querySelector(sel); | |
| 18 | + | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
| 19 | + | const AUTO = '.dlazy[data-src]:not([data-manual])'; | |
| 20 | + | const loading = new Map(); // src -> promise of the swap | |
| 21 | + | let moved = false; | |
| 22 | + | let lazyIO = null; // the observer of the automatic placeholders | |
| 23 | + | let generation = 0; // bumped by reset: older responses are ignored | |
| 24 | + | let onSwap = () => {}; | |
| 25 | + | ||
| 26 | + | // load fetches the fragment of a placeholder and swaps its files in. The | |
| 27 | + | // promise resolves once they are in the page (or the page said to reload). | |
| 28 | + | function load(ph) { | |
| 29 | + | const gen = generation; | |
| 30 | + | const src = ph?.dataset.src; | |
| 31 | + | if (!src || moved) return Promise.resolve(); | |
| 32 | + | if (loading.has(src)) return loading.get(src); | |
| 33 | + | for (const p of $$('.dlazy[data-src]')) if (p.dataset.src === src) p.setAttribute('data-loading', ''); | |
| 34 | + | // The fragment renders like the page: the layout shown, and both layouts | |
| 35 | + | // when the page switches them itself (data-diff) or one (data-view). | |
| 36 | + | const d = $('#diffs')?.dataset || {}; | |
| 37 | + | const view = d.diff || d.view; | |
| 38 | + | const layouts = d.diff ? 'both' : d.view ? 'one' : ''; | |
| 39 | + | const url = src + (view ? '&view=' + encodeURIComponent(view) : '') + (layouts ? '&layouts=' + layouts : ''); | |
| 40 | + | const p = fetch(url, { headers: { Accept: 'text/html' }, credentials: 'same-origin' }) | |
| 41 | + | .then(async (res) => { | |
| 42 | + | const html = await res.text(); | |
| 43 | + | if (gen !== generation) return; | |
| 44 | + | if (res.status === 409) return stale(html); | |
| 45 | + | if (!res.ok) throw new Error('loading files: ' + res.status); | |
| 46 | + | // A fragment of the old diff after the page went stale stays out. | |
| 47 | + | if (!moved) swap(html); | |
| 48 | + | }) | |
| 49 | + | .catch((err) => { | |
| 50 | + | if (gen !== generation) return; | |
| 51 | + | loading.delete(src); | |
| 52 | + | for (const p of $$('.dlazy[data-loading]')) if (p.dataset.src === src) p.removeAttribute('data-loading'); | |
| 53 | + | window.ermite?.flash?.(err.message, 4000); | |
| 54 | + | }); | |
| 55 | + | loading.set(src, p); | |
| 56 | + | return p; | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // swap replaces each placeholder file with the file of the same id. A | |
| 60 | + | // file the reader folded stays folded. | |
| 61 | + | function swap(html) { | |
| 62 | + | const t = document.createElement('template'); | |
| 63 | + | t.innerHTML = html; | |
| 64 | + | for (const f of [...t.content.querySelectorAll('details[data-file]')]) { | |
| 65 | + | const old = document.getElementById(f.id); | |
| 66 | + | if (!old || !old.querySelector('.dlazy')) continue; | |
| 67 | + | // A collapsed file opens once loaded; a file of a window keeps the fold. | |
| 68 | + | f.open = old.querySelector('.dlazy[data-manual]') ? true : old.open; | |
| 69 | + | for (const p of $$('.dlazy', old)) lazyIO?.unobserve(p); | |
| 70 | + | old.replaceWith(f); | |
| 71 | + | window.htmx?.process(f); | |
| 72 | + | window.ermite?.watchFile?.(f, old); | |
| 73 | + | } | |
| 74 | + | onSwap(); | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // stale: the diff changed under the page. The placeholders say so; no file | |
| 78 | + | // of another diff comes in. | |
| 79 | + | function stale(html) { | |
| 80 | + | moved = true; | |
| 81 | + | for (const p of $$('.dlazy')) { | |
| 82 | + | p.removeAttribute('data-loading'); | |
| 83 | + | p.innerHTML = html; | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // Automatic placeholders load as they come within two screens of the view; | |
| 88 | + | // files that come in are marked in the list of files (ermite.js marks the | |
| 89 | + | // one in view). Manual ones are left alone. | |
| 90 | + | function watch() { | |
| 91 | + | lazyIO?.disconnect(); | |
| 92 | + | if (!('IntersectionObserver' in window)) { | |
| 93 | + | for (const p of $$(AUTO)) load(p); | |
| 94 | + | return; | |
| 95 | + | } | |
| 96 | + | lazyIO = new IntersectionObserver((entries) => { | |
| 97 | + | for (const e of entries) { | |
| 98 | + | if (!e.isIntersecting) continue; | |
| 99 | + | lazyIO.unobserve(e.target); | |
| 100 | + | load(e.target); | |
| 101 | + | } | |
| 102 | + | }, { rootMargin: '200% 0px 200% 0px' }); | |
| 103 | + | for (const p of $$(AUTO)) lazyIO.observe(p); | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // reset forgets the loads in flight and the stale state: the page shows | |
| 107 | + | // another diff (refresh in place). watch starts the observer again. | |
| 108 | + | function reset() { | |
| 109 | + | generation++; | |
| 110 | + | moved = false; | |
| 111 | + | loading.clear(); | |
| 112 | + | lazyIO?.disconnect(); | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // manualLink is the "load diff" link of an event target, when it is one that | |
| 116 | + | // loads in place (the placeholder has an address). | |
| 117 | + | const manualLink = (t) => { | |
| 118 | + | const a = t?.closest?.('.dlazy[data-manual] > a'); | |
| 119 | + | return a && a.closest('.dlazy')?.dataset.src ? a : null; | |
| 120 | + | }; | |
| 121 | + | ||
| 122 | + | // loadManual loads the file of a "load diff" link and puts the focus in it: | |
| 123 | + | // on its first line link, else on its header. The scroll never moves for it. | |
| 124 | + | function loadManual(a) { | |
| 125 | + | const ph = a.closest('.dlazy'); | |
| 126 | + | const id = ph.closest('details')?.id; | |
| 127 | + | return load(ph).then(() => { | |
| 128 | + | const f = id && document.getElementById(id); | |
| 129 | + | if (!f || f.querySelector('.dlazy[data-manual]')) return; | |
| 130 | + | (f.querySelector('.ln a') || f.querySelector('summary'))?.focus({ preventScroll: true }); | |
| 131 | + | }); | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | function onClick(e) { | |
| 135 | + | if (e.defaultPrevented || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return false; | |
| 136 | + | const a = manualLink(e.target); | |
| 137 | + | if (!a) return false; | |
| 138 | + | e.preventDefault(); | |
| 139 | + | loadManual(a); | |
| 140 | + | return true; | |
| 141 | + | } | |
| 142 | + | // Enter makes the browser click a link by itself; Space does not. Both are | |
| 143 | + | // handled here, once (a fragment is fetched once). | |
| 144 | + | function onKey(e) { | |
| 145 | + | if ((e.key !== 'Enter' && e.key !== ' ') || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return false; | |
| 146 | + | const a = manualLink(e.target); | |
| 147 | + | if (!a) return false; | |
| 148 | + | e.preventDefault(); | |
| 149 | + | loadManual(a); | |
| 150 | + | return true; | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | let started = false; | |
| 154 | + | function start() { | |
| 155 | + | if (started || !$('#diffs')) return; | |
| 156 | + | started = true; | |
| 157 | + | watch(); | |
| 158 | + | document.addEventListener('click', onClick); | |
| 159 | + | document.addEventListener('keydown', onKey); | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | return { load, start, watch, reset, onSwap: (fn) => { onSwap = fn; }, onClick, onKey }; | |
| 163 | + | })(); | |
| 164 | + | ||
| 165 | + | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteLazy.start()); | |
| 166 | + | else window.ermiteLazy.start(); | |
| @@ -0,0 +1,166 @@ | |||||
| 1 | + | // lazy.js: the files of a diff that are not in the page yet (commit, compare | |||
| 2 | + | // and merge request pages). | |||
| 3 | + | // | |||
| 4 | + | // A placeholder is a .dlazy[data-src] in a details.dfile. Two kinds: | |||
| 5 | + | // - a window of a merge request: loaded by itself when it comes within two | |||
| 6 | + | // screens of the view (the files of its window replace the placeholders of | |||
| 7 | + | // the same ids); | |||
| 8 | + | // - a collapsed file (generated, large) [data-manual]: never loaded by | |||
| 9 | + | // itself. Its "load diff" link loads that one file (click, or Enter / Space | |||
| 10 | + | // on the link); without scripts the link reloads the page with the file. | |||
| 11 | + | // A fragment is fetched once. Nothing observes the DOM: the one | |||
| 12 | + | // IntersectionObserver watches the automatic placeholders, and a manual one | |||
| 13 | + | // costs no more than its markup. | |||
| 14 | + | // | |||
| 15 | + | // window.ermiteLazy.load is also the loader of mr.js (reveal). | |||
| 16 | + | window.ermiteLazy = (() => { | |||
| 17 | + | const $ = (sel, root = document) => root.querySelector(sel); | |||
| 18 | + | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |||
| 19 | + | const AUTO = '.dlazy[data-src]:not([data-manual])'; | |||
| 20 | + | const loading = new Map(); // src -> promise of the swap | |||
| 21 | + | let moved = false; | |||
| 22 | + | let lazyIO = null; // the observer of the automatic placeholders | |||
| 23 | + | let generation = 0; // bumped by reset: older responses are ignored | |||
| 24 | + | let onSwap = () => {}; | |||
| 25 | + | ||||
| 26 | + | // load fetches the fragment of a placeholder and swaps its files in. The | |||
| 27 | + | // promise resolves once they are in the page (or the page said to reload). | |||
| 28 | + | function load(ph) { | |||
| 29 | + | const gen = generation; | |||
| 30 | + | const src = ph?.dataset.src; | |||
| 31 | + | if (!src || moved) return Promise.resolve(); | |||
| 32 | + | if (loading.has(src)) return loading.get(src); | |||
| 33 | + | for (const p of $$('.dlazy[data-src]')) if (p.dataset.src === src) p.setAttribute('data-loading', ''); | |||
| 34 | + | // The fragment renders like the page: the layout shown, and both layouts | |||
| 35 | + | // when the page switches them itself (data-diff) or one (data-view). | |||
| 36 | + | const d = $('#diffs')?.dataset || {}; | |||
| 37 | + | const view = d.diff || d.view; | |||
| 38 | + | const layouts = d.diff ? 'both' : d.view ? 'one' : ''; | |||
| 39 | + | const url = src + (view ? '&view=' + encodeURIComponent(view) : '') + (layouts ? '&layouts=' + layouts : ''); | |||
| 40 | + | const p = fetch(url, { headers: { Accept: 'text/html' }, credentials: 'same-origin' }) | |||
| 41 | + | .then(async (res) => { | |||
| 42 | + | const html = await res.text(); | |||
| 43 | + | if (gen !== generation) return; | |||
| 44 | + | if (res.status === 409) return stale(html); | |||
| 45 | + | if (!res.ok) throw new Error('loading files: ' + res.status); | |||
| 46 | + | // A fragment of the old diff after the page went stale stays out. | |||
| 47 | + | if (!moved) swap(html); | |||
| 48 | + | }) | |||
| 49 | + | .catch((err) => { | |||
| 50 | + | if (gen !== generation) return; | |||
| 51 | + | loading.delete(src); | |||
| 52 | + | for (const p of $$('.dlazy[data-loading]')) if (p.dataset.src === src) p.removeAttribute('data-loading'); | |||
| 53 | + | window.ermite?.flash?.(err.message, 4000); | |||
| 54 | + | }); | |||
| 55 | + | loading.set(src, p); | |||
| 56 | + | return p; | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // swap replaces each placeholder file with the file of the same id. A | |||
| 60 | + | // file the reader folded stays folded. | |||
| 61 | + | function swap(html) { | |||
| 62 | + | const t = document.createElement('template'); | |||
| 63 | + | t.innerHTML = html; | |||
| 64 | + | for (const f of [...t.content.querySelectorAll('details[data-file]')]) { | |||
| 65 | + | const old = document.getElementById(f.id); | |||
| 66 | + | if (!old || !old.querySelector('.dlazy')) continue; | |||
| 67 | + | // A collapsed file opens once loaded; a file of a window keeps the fold. | |||
| 68 | + | f.open = old.querySelector('.dlazy[data-manual]') ? true : old.open; | |||
| 69 | + | for (const p of $$('.dlazy', old)) lazyIO?.unobserve(p); | |||
| 70 | + | old.replaceWith(f); | |||
| 71 | + | window.htmx?.process(f); | |||
| 72 | + | window.ermite?.watchFile?.(f, old); | |||
| 73 | + | } | |||
| 74 | + | onSwap(); | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // stale: the diff changed under the page. The placeholders say so; no file | |||
| 78 | + | // of another diff comes in. | |||
| 79 | + | function stale(html) { | |||
| 80 | + | moved = true; | |||
| 81 | + | for (const p of $$('.dlazy')) { | |||
| 82 | + | p.removeAttribute('data-loading'); | |||
| 83 | + | p.innerHTML = html; | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // Automatic placeholders load as they come within two screens of the view; | |||
| 88 | + | // files that come in are marked in the list of files (ermite.js marks the | |||
| 89 | + | // one in view). Manual ones are left alone. | |||
| 90 | + | function watch() { | |||
| 91 | + | lazyIO?.disconnect(); | |||
| 92 | + | if (!('IntersectionObserver' in window)) { | |||
| 93 | + | for (const p of $$(AUTO)) load(p); | |||
| 94 | + | return; | |||
| 95 | + | } | |||
| 96 | + | lazyIO = new IntersectionObserver((entries) => { | |||
| 97 | + | for (const e of entries) { | |||
| 98 | + | if (!e.isIntersecting) continue; | |||
| 99 | + | lazyIO.unobserve(e.target); | |||
| 100 | + | load(e.target); | |||
| 101 | + | } | |||
| 102 | + | }, { rootMargin: '200% 0px 200% 0px' }); | |||
| 103 | + | for (const p of $$(AUTO)) lazyIO.observe(p); | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // reset forgets the loads in flight and the stale state: the page shows | |||
| 107 | + | // another diff (refresh in place). watch starts the observer again. | |||
| 108 | + | function reset() { | |||
| 109 | + | generation++; | |||
| 110 | + | moved = false; | |||
| 111 | + | loading.clear(); | |||
| 112 | + | lazyIO?.disconnect(); | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | // manualLink is the "load diff" link of an event target, when it is one that | |||
| 116 | + | // loads in place (the placeholder has an address). | |||
| 117 | + | const manualLink = (t) => { | |||
| 118 | + | const a = t?.closest?.('.dlazy[data-manual] > a'); | |||
| 119 | + | return a && a.closest('.dlazy')?.dataset.src ? a : null; | |||
| 120 | + | }; | |||
| 121 | + | ||||
| 122 | + | // loadManual loads the file of a "load diff" link and puts the focus in it: | |||
| 123 | + | // on its first line link, else on its header. The scroll never moves for it. | |||
| 124 | + | function loadManual(a) { | |||
| 125 | + | const ph = a.closest('.dlazy'); | |||
| 126 | + | const id = ph.closest('details')?.id; | |||
| 127 | + | return load(ph).then(() => { | |||
| 128 | + | const f = id && document.getElementById(id); | |||
| 129 | + | if (!f || f.querySelector('.dlazy[data-manual]')) return; | |||
| 130 | + | (f.querySelector('.ln a') || f.querySelector('summary'))?.focus({ preventScroll: true }); | |||
| 131 | + | }); | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | function onClick(e) { | |||
| 135 | + | if (e.defaultPrevented || e.button || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return false; | |||
| 136 | + | const a = manualLink(e.target); | |||
| 137 | + | if (!a) return false; | |||
| 138 | + | e.preventDefault(); | |||
| 139 | + | loadManual(a); | |||
| 140 | + | return true; | |||
| 141 | + | } | |||
| 142 | + | // Enter makes the browser click a link by itself; Space does not. Both are | |||
| 143 | + | // handled here, once (a fragment is fetched once). | |||
| 144 | + | function onKey(e) { | |||
| 145 | + | if ((e.key !== 'Enter' && e.key !== ' ') || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return false; | |||
| 146 | + | const a = manualLink(e.target); | |||
| 147 | + | if (!a) return false; | |||
| 148 | + | e.preventDefault(); | |||
| 149 | + | loadManual(a); | |||
| 150 | + | return true; | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | let started = false; | |||
| 154 | + | function start() { | |||
| 155 | + | if (started || !$('#diffs')) return; | |||
| 156 | + | started = true; | |||
| 157 | + | watch(); | |||
| 158 | + | document.addEventListener('click', onClick); | |||
| 159 | + | document.addEventListener('keydown', onKey); | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | return { load, start, watch, reset, onSwap: (fn) => { onSwap = fn; }, onClick, onKey }; | |||
| 163 | + | })(); | |||
| 164 | + | ||||
| 165 | + | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteLazy.start()); | |||
| 166 | + | else window.ermiteLazy.start(); | |||
M internal/adapters/web/static/js/mr.js +8 -91
| @@ -1,10 +1,9 @@ | |||
| 1 | 1 | // mr.js: the merge request page beyond what htmx and hx-live give. | |
| 2 | 2 | // | |
| 3 | 3 | // 1. Windows of files. The page holds the hunks of one window of files; the | |
| 4 | - | // other files are placeholders (.dlazy[data-src]). A placeholder that comes | |
| 5 | - | // near the screen fetches its window (/files?from=&base=&head=), whose | |
| 6 | - | // files replace the placeholders of the same ids. A window is fetched once; | |
| 7 | - | // a link to a file or to a comment of a placeholder fetches it first. | |
| 4 | + | // other files are placeholders (.dlazy[data-src]), loaded by lazy.js (which | |
| 5 | + | // the commit and compare pages use too) when they come near the screen. A | |
| 6 | + | // link to a file or to a comment of a placeholder loads it first (reveal). | |
| 8 | 7 | // 2. The menus of the bar (merge, more) and their keys: M opens the merge menu | |
| 9 | 8 | // on its button, 1-9 pick a mode, D toggles the branch deletion, Enter | |
| 10 | 9 | // submits (the browser does it: the focus is on the button), Esc closes. | |
| @@ -80,72 +79,10 @@ window.ermiteMR = (() => { | |||
| 80 | 79 | ||
| 81 | 80 | const $ = (sel, root = document) => root.querySelector(sel); | |
| 82 | 81 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
| 83 | - | const loading = new Map(); // src -> promise of the swap | |
| 84 | 82 | let refreshTouchComments = () => {}; | |
| 85 | - | let moved = false; | |
| 86 | - | let lazyIO = null; // the observer of the placeholders (watch) | |
| 87 | - | let diffGeneration = 0; | |
| 88 | 83 | let revealGen = 0; // the last reveal wins the scroll | |
| 89 | - | ||
| 90 | - | // load fetches the window of a placeholder and swaps its files in. The | |
| 91 | - | // promise resolves once they are in the page (or the page said to reload). | |
| 92 | - | function load(ph) { | |
| 93 | - | const generation = diffGeneration; | |
| 94 | - | const src = ph?.dataset.src; | |
| 95 | - | if (!src || moved) return Promise.resolve(); | |
| 96 | - | if (loading.has(src)) return loading.get(src); | |
| 97 | - | for (const p of $$('.dlazy[data-src]')) if (p.dataset.src === src) p.setAttribute('data-loading', ''); | |
| 98 | - | // The window renders like the page: the layout shown, and both layouts | |
| 99 | - | // when the page switches them itself (data-diff) or one (data-view). | |
| 100 | - | const d = $('#diffs')?.dataset || {}; | |
| 101 | - | const view = d.diff || d.view; | |
| 102 | - | const layouts = d.diff ? 'both' : d.view ? 'one' : ''; | |
| 103 | - | const url = src + (view ? '&view=' + encodeURIComponent(view) : '') + (layouts ? '&layouts=' + layouts : ''); | |
| 104 | - | const p = fetch(url, { headers: { Accept: 'text/html' }, credentials: 'same-origin' }) | |
| 105 | - | .then(async (res) => { | |
| 106 | - | const html = await res.text(); | |
| 107 | - | if (generation !== diffGeneration) return; | |
| 108 | - | if (res.status === 409) return stale(html); | |
| 109 | - | if (!res.ok) throw new Error('loading files: ' + res.status); | |
| 110 | - | // A window of the old diff after the page went stale stays out. | |
| 111 | - | if (!moved) swap(html); | |
| 112 | - | }) | |
| 113 | - | .catch((err) => { | |
| 114 | - | if (generation !== diffGeneration) return; | |
| 115 | - | loading.delete(src); | |
| 116 | - | for (const p of $$('.dlazy[data-loading]')) if (p.dataset.src === src) p.removeAttribute('data-loading'); | |
| 117 | - | window.ermite?.flash?.(err.message, 4000); | |
| 118 | - | }); | |
| 119 | - | loading.set(src, p); | |
| 120 | - | return p; | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | // swap replaces each placeholder file with the file of the same id. A | |
| 124 | - | // file the reader folded stays folded. | |
| 125 | - | function swap(html) { | |
| 126 | - | const t = document.createElement('template'); | |
| 127 | - | t.innerHTML = html; | |
| 128 | - | for (const f of [...t.content.querySelectorAll('details[data-file]')]) { | |
| 129 | - | const old = document.getElementById(f.id); | |
| 130 | - | if (!old || !old.querySelector('.dlazy')) continue; | |
| 131 | - | f.open = old.open; | |
| 132 | - | for (const p of $$('.dlazy', old)) lazyIO?.unobserve(p); | |
| 133 | - | old.replaceWith(f); | |
| 134 | - | window.htmx?.process(f); | |
| 135 | - | window.ermite?.watchFile?.(f, old); | |
| 136 | - | } | |
| 137 | - | refreshTouchComments(); | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | // stale: the diff changed under the page. The placeholders say so; no file | |
| 141 | - | // of another diff comes in. | |
| 142 | - | function stale(html) { | |
| 143 | - | moved = true; | |
| 144 | - | for (const p of $$('.dlazy')) { | |
| 145 | - | p.removeAttribute('data-loading'); | |
| 146 | - | p.innerHTML = html; | |
| 147 | - | } | |
| 148 | - | } | |
| 84 | + | const load = (ph) => window.ermiteLazy.load(ph); | |
| 85 | + | window.ermiteLazy.onSwap(() => refreshTouchComments()); | |
| 149 | 86 | ||
| 150 | 87 | // reveal loads the file of an id (a file "f12" or a comment "c3") when it is | |
| 151 | 88 | // a placeholder, then scrolls to it. It returns false when there is nothing | |
| @@ -163,7 +100,7 @@ window.ermiteMR = (() => { | |||
| 163 | 100 | } | |
| 164 | 101 | return document.getElementById(id); | |
| 165 | 102 | }; | |
| 166 | - | let ph = document.getElementById(id)?.querySelector(':scope > .dlazy'); | |
| 103 | + | let ph = document.getElementById(id)?.querySelector(':scope > .dlazy:not([data-manual])'); | |
| 167 | 104 | if (!ph && /^c\d+/.test(id) && !document.getElementById(id)) { | |
| 168 | 105 | const base = id.replace(/-s$/, ''); | |
| 169 | 106 | ph = $$('.dlazy[data-notes]').find((p) => p.dataset.notes.split(' ').includes(base)); | |
| @@ -195,25 +132,6 @@ window.ermiteMR = (() => { | |||
| 195 | 132 | return true; | |
| 196 | 133 | } | |
| 197 | 134 | ||
| 198 | - | // Placeholders load as they come within two screens of the view; files | |
| 199 | - | // that come in are marked in the list of files (ermite.js marks the one in | |
| 200 | - | // view). | |
| 201 | - | function watch() { | |
| 202 | - | lazyIO?.disconnect(); | |
| 203 | - | if (!('IntersectionObserver' in window)) { | |
| 204 | - | for (const p of $$('.dlazy[data-src]')) load(p); | |
| 205 | - | return; | |
| 206 | - | } | |
| 207 | - | lazyIO = new IntersectionObserver((entries) => { | |
| 208 | - | for (const e of entries) { | |
| 209 | - | if (!e.isIntersecting) continue; | |
| 210 | - | lazyIO.unobserve(e.target); | |
| 211 | - | load(e.target); | |
| 212 | - | } | |
| 213 | - | }, { rootMargin: '200% 0px 200% 0px' }); | |
| 214 | - | for (const p of $$('.dlazy[data-src]')) lazyIO.observe(p); | |
| 215 | - | } | |
| 216 | - | ||
| 217 | 135 | // The merge menu. | |
| 218 | 136 | const menu = () => $('#merge-menu'); | |
| 219 | 137 | function openMerge() { | |
| @@ -598,7 +516,7 @@ window.ermiteMR = (() => { | |||
| 598 | 516 | const active = document.activeElement; | |
| 599 | 517 | const focus = active && { file: active.closest?.('.files a')?.getAttribute('title'), region: regions.find((sel) => $(sel)?.contains(active)), menu: active.closest?.('details.menu')?.id, id: active.id, tag: active.tagName, name: active.getAttribute('name'), href: active.getAttribute('href') }; | |
| 600 | 518 | for (const f of $$('#diffs details.dfile', page)) if (files.has(f.dataset.file)) f.open = files.get(f.dataset.file); | |
| 601 | - | ++diffGeneration; ++revealGen; moved = false; loading.clear(); lazyIO?.disconnect(); | |
| 519 | + | ++revealGen; window.ermiteLazy.reset(); | |
| 602 | 520 | $('#diffs', page).dataset.mrStarted = 'true'; | |
| 603 | 521 | // The fresh footer comes in with the panels as they are, so that an open | |
| 604 | 522 | // panel does not fold and open again while the other regions swap. | |
| @@ -624,7 +542,7 @@ window.ermiteMR = (() => { | |||
| 624 | 542 | touchComments(false); | |
| 625 | 543 | $('#diffs').dataset.mobile = String(!!window.matchMedia?.('(max-width: 720px)').matches); | |
| 626 | 544 | window.ermite?.refreshFiles?.(); | |
| 627 | - | watch(); | |
| 545 | + | window.ermiteLazy.watch(); | |
| 628 | 546 | if (focus && active !== document.body) { | |
| 629 | 547 | const region = focus.region ? $(focus.region) : document; | |
| 630 | 548 | const menu = focus.menu && document.getElementById(focus.menu); | |
| @@ -700,7 +618,6 @@ window.ermiteMR = (() => { | |||
| 700 | 618 | // the address of the request, so that a reload does not post again. | |
| 701 | 619 | const canonical = $('#diffs[data-canonical]')?.dataset.canonical; | |
| 702 | 620 | if (canonical && location.pathname !== canonical) history.replaceState(history.state, '', canonical + location.hash); | |
| 703 | - | watch(); | |
| 704 | 621 | // A link to a file or a comment that is not loaded yet. | |
| 705 | 622 | const hash = () => { | |
| 706 | 623 | const id = decodeURIComponent(location.hash.slice(1)); | |
| @@ -1,10 +1,9 @@ | |||||
| 1 | // mr.js: the merge request page beyond what htmx and hx-live give. | 1 | // mr.js: the merge request page beyond what htmx and hx-live give. | ||
| 2 | // | 2 | // | ||
| 3 | // 1. Windows of files. The page holds the hunks of one window of files; the | 3 | // 1. Windows of files. The page holds the hunks of one window of files; the | ||
| 4 | - | // other files are placeholders (.dlazy[data-src]). A placeholder that comes | 4 | + | // other files are placeholders (.dlazy[data-src]), loaded by lazy.js (which |
| 5 | - | // near the screen fetches its window (/files?from=&base=&head=), whose | 5 | + | // the commit and compare pages use too) when they come near the screen. A |
| 6 | - | // files replace the placeholders of the same ids. A window is fetched once; | 6 | + | // link to a file or to a comment of a placeholder loads it first (reveal). |
| 7 | - | // a link to a file or to a comment of a placeholder fetches it first. | |||
| 8 | // 2. The menus of the bar (merge, more) and their keys: M opens the merge menu | 7 | // 2. The menus of the bar (merge, more) and their keys: M opens the merge menu | ||
| 9 | // on its button, 1-9 pick a mode, D toggles the branch deletion, Enter | 8 | // on its button, 1-9 pick a mode, D toggles the branch deletion, Enter | ||
| 10 | // submits (the browser does it: the focus is on the button), Esc closes. | 9 | // submits (the browser does it: the focus is on the button), Esc closes. | ||
| @@ -80,72 +79,10 @@ window.ermiteMR = (() => { | |||||
| 80 | 79 | ||||
| 81 | const $ = (sel, root = document) => root.querySelector(sel); | 80 | const $ = (sel, root = document) => root.querySelector(sel); | ||
| 82 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | 81 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | ||
| 83 | - | const loading = new Map(); // src -> promise of the swap | |||
| 84 | let refreshTouchComments = () => {}; | 82 | let refreshTouchComments = () => {}; | ||
| 85 | - | let moved = false; | |||
| 86 | - | let lazyIO = null; // the observer of the placeholders (watch) | |||
| 87 | - | let diffGeneration = 0; | |||
| 88 | let revealGen = 0; // the last reveal wins the scroll | 83 | let revealGen = 0; // the last reveal wins the scroll | ||
| 89 | - | 84 | + | const load = (ph) => window.ermiteLazy.load(ph); | |
| 90 | - | // load fetches the window of a placeholder and swaps its files in. The | 85 | + | window.ermiteLazy.onSwap(() => refreshTouchComments()); |
| 91 | - | // promise resolves once they are in the page (or the page said to reload). | |||
| 92 | - | function load(ph) { | |||
| 93 | - | const generation = diffGeneration; | |||
| 94 | - | const src = ph?.dataset.src; | |||
| 95 | - | if (!src || moved) return Promise.resolve(); | |||
| 96 | - | if (loading.has(src)) return loading.get(src); | |||
| 97 | - | for (const p of $$('.dlazy[data-src]')) if (p.dataset.src === src) p.setAttribute('data-loading', ''); | |||
| 98 | - | // The window renders like the page: the layout shown, and both layouts | |||
| 99 | - | // when the page switches them itself (data-diff) or one (data-view). | |||
| 100 | - | const d = $('#diffs')?.dataset || {}; | |||
| 101 | - | const view = d.diff || d.view; | |||
| 102 | - | const layouts = d.diff ? 'both' : d.view ? 'one' : ''; | |||
| 103 | - | const url = src + (view ? '&view=' + encodeURIComponent(view) : '') + (layouts ? '&layouts=' + layouts : ''); | |||
| 104 | - | const p = fetch(url, { headers: { Accept: 'text/html' }, credentials: 'same-origin' }) | |||
| 105 | - | .then(async (res) => { | |||
| 106 | - | const html = await res.text(); | |||
| 107 | - | if (generation !== diffGeneration) return; | |||
| 108 | - | if (res.status === 409) return stale(html); | |||
| 109 | - | if (!res.ok) throw new Error('loading files: ' + res.status); | |||
| 110 | - | // A window of the old diff after the page went stale stays out. | |||
| 111 | - | if (!moved) swap(html); | |||
| 112 | - | }) | |||
| 113 | - | .catch((err) => { | |||
| 114 | - | if (generation !== diffGeneration) return; | |||
| 115 | - | loading.delete(src); | |||
| 116 | - | for (const p of $$('.dlazy[data-loading]')) if (p.dataset.src === src) p.removeAttribute('data-loading'); | |||
| 117 | - | window.ermite?.flash?.(err.message, 4000); | |||
| 118 | - | }); | |||
| 119 | - | loading.set(src, p); | |||
| 120 | - | return p; | |||
| 121 | - | } | |||
| 122 | - | ||||
| 123 | - | // swap replaces each placeholder file with the file of the same id. A | |||
| 124 | - | // file the reader folded stays folded. | |||
| 125 | - | function swap(html) { | |||
| 126 | - | const t = document.createElement('template'); | |||
| 127 | - | t.innerHTML = html; | |||
| 128 | - | for (const f of [...t.content.querySelectorAll('details[data-file]')]) { | |||
| 129 | - | const old = document.getElementById(f.id); | |||
| 130 | - | if (!old || !old.querySelector('.dlazy')) continue; | |||
| 131 | - | f.open = old.open; | |||
| 132 | - | for (const p of $$('.dlazy', old)) lazyIO?.unobserve(p); | |||
| 133 | - | old.replaceWith(f); | |||
| 134 | - | window.htmx?.process(f); | |||
| 135 | - | window.ermite?.watchFile?.(f, old); | |||
| 136 | - | } | |||
| 137 | - | refreshTouchComments(); | |||
| 138 | - | } | |||
| 139 | - | ||||
| 140 | - | // stale: the diff changed under the page. The placeholders say so; no file | |||
| 141 | - | // of another diff comes in. | |||
| 142 | - | function stale(html) { | |||
| 143 | - | moved = true; | |||
| 144 | - | for (const p of $$('.dlazy')) { | |||
| 145 | - | p.removeAttribute('data-loading'); | |||
| 146 | - | p.innerHTML = html; | |||
| 147 | - | } | |||
| 148 | - | } | |||
| 149 | 86 | ||||
| 150 | // reveal loads the file of an id (a file "f12" or a comment "c3") when it is | 87 | // reveal loads the file of an id (a file "f12" or a comment "c3") when it is | ||
| 151 | // a placeholder, then scrolls to it. It returns false when there is nothing | 88 | // a placeholder, then scrolls to it. It returns false when there is nothing | ||
| @@ -163,7 +100,7 @@ window.ermiteMR = (() => { | |||||
| 163 | } | 100 | } | ||
| 164 | return document.getElementById(id); | 101 | return document.getElementById(id); | ||
| 165 | }; | 102 | }; | ||
| 166 | - | let ph = document.getElementById(id)?.querySelector(':scope > .dlazy'); | 103 | + | let ph = document.getElementById(id)?.querySelector(':scope > .dlazy:not([data-manual])'); |
| 167 | if (!ph && /^c\d+/.test(id) && !document.getElementById(id)) { | 104 | if (!ph && /^c\d+/.test(id) && !document.getElementById(id)) { | ||
| 168 | const base = id.replace(/-s$/, ''); | 105 | const base = id.replace(/-s$/, ''); | ||
| 169 | ph = $$('.dlazy[data-notes]').find((p) => p.dataset.notes.split(' ').includes(base)); | 106 | ph = $$('.dlazy[data-notes]').find((p) => p.dataset.notes.split(' ').includes(base)); | ||
| @@ -195,25 +132,6 @@ window.ermiteMR = (() => { | |||||
| 195 | return true; | 132 | return true; | ||
| 196 | } | 133 | } | ||
| 197 | 134 | ||||
| 198 | - | // Placeholders load as they come within two screens of the view; files | |||
| 199 | - | // that come in are marked in the list of files (ermite.js marks the one in | |||
| 200 | - | // view). | |||
| 201 | - | function watch() { | |||
| 202 | - | lazyIO?.disconnect(); | |||
| 203 | - | if (!('IntersectionObserver' in window)) { | |||
| 204 | - | for (const p of $$('.dlazy[data-src]')) load(p); | |||
| 205 | - | return; | |||
| 206 | - | } | |||
| 207 | - | lazyIO = new IntersectionObserver((entries) => { | |||
| 208 | - | for (const e of entries) { | |||
| 209 | - | if (!e.isIntersecting) continue; | |||
| 210 | - | lazyIO.unobserve(e.target); | |||
| 211 | - | load(e.target); | |||
| 212 | - | } | |||
| 213 | - | }, { rootMargin: '200% 0px 200% 0px' }); | |||
| 214 | - | for (const p of $$('.dlazy[data-src]')) lazyIO.observe(p); | |||
| 215 | - | } | |||
| 216 | - | ||||
| 217 | // The merge menu. | 135 | // The merge menu. | ||
| 218 | const menu = () => $('#merge-menu'); | 136 | const menu = () => $('#merge-menu'); | ||
| 219 | function openMerge() { | 137 | function openMerge() { | ||
| @@ -598,7 +516,7 @@ window.ermiteMR = (() => { | |||||
| 598 | const active = document.activeElement; | 516 | const active = document.activeElement; | ||
| 599 | const focus = active && { file: active.closest?.('.files a')?.getAttribute('title'), region: regions.find((sel) => $(sel)?.contains(active)), menu: active.closest?.('details.menu')?.id, id: active.id, tag: active.tagName, name: active.getAttribute('name'), href: active.getAttribute('href') }; | 517 | const focus = active && { file: active.closest?.('.files a')?.getAttribute('title'), region: regions.find((sel) => $(sel)?.contains(active)), menu: active.closest?.('details.menu')?.id, id: active.id, tag: active.tagName, name: active.getAttribute('name'), href: active.getAttribute('href') }; | ||
| 600 | for (const f of $$('#diffs details.dfile', page)) if (files.has(f.dataset.file)) f.open = files.get(f.dataset.file); | 518 | for (const f of $$('#diffs details.dfile', page)) if (files.has(f.dataset.file)) f.open = files.get(f.dataset.file); | ||
| 601 | - | ++diffGeneration; ++revealGen; moved = false; loading.clear(); lazyIO?.disconnect(); | 519 | + | ++revealGen; window.ermiteLazy.reset(); |
| 602 | $('#diffs', page).dataset.mrStarted = 'true'; | 520 | $('#diffs', page).dataset.mrStarted = 'true'; | ||
| 603 | // The fresh footer comes in with the panels as they are, so that an open | 521 | // The fresh footer comes in with the panels as they are, so that an open | ||
| 604 | // panel does not fold and open again while the other regions swap. | 522 | // panel does not fold and open again while the other regions swap. | ||
| @@ -624,7 +542,7 @@ window.ermiteMR = (() => { | |||||
| 624 | touchComments(false); | 542 | touchComments(false); | ||
| 625 | $('#diffs').dataset.mobile = String(!!window.matchMedia?.('(max-width: 720px)').matches); | 543 | $('#diffs').dataset.mobile = String(!!window.matchMedia?.('(max-width: 720px)').matches); | ||
| 626 | window.ermite?.refreshFiles?.(); | 544 | window.ermite?.refreshFiles?.(); | ||
| 627 | - | watch(); | 545 | + | window.ermiteLazy.watch(); |
| 628 | if (focus && active !== document.body) { | 546 | if (focus && active !== document.body) { | ||
| 629 | const region = focus.region ? $(focus.region) : document; | 547 | const region = focus.region ? $(focus.region) : document; | ||
| 630 | const menu = focus.menu && document.getElementById(focus.menu); | 548 | const menu = focus.menu && document.getElementById(focus.menu); | ||
| @@ -700,7 +618,6 @@ window.ermiteMR = (() => { | |||||
| 700 | // the address of the request, so that a reload does not post again. | 618 | // the address of the request, so that a reload does not post again. | ||
| 701 | const canonical = $('#diffs[data-canonical]')?.dataset.canonical; | 619 | const canonical = $('#diffs[data-canonical]')?.dataset.canonical; | ||
| 702 | if (canonical && location.pathname !== canonical) history.replaceState(history.state, '', canonical + location.hash); | 620 | if (canonical && location.pathname !== canonical) history.replaceState(history.state, '', canonical + location.hash); | ||
| 703 | - | watch(); | |||
| 704 | // A link to a file or a comment that is not loaded yet. | 621 | // A link to a file or a comment that is not loaded yet. | ||
| 705 | const hash = () => { | 622 | const hash = () => { | ||
| 706 | const id = decodeURIComponent(location.hash.slice(1)); | 623 | const id = decodeURIComponent(location.hash.slice(1)); | ||
M internal/adapters/web/view.go +3 -0
| @@ -67,6 +67,9 @@ type Layout struct { | |||
| 67 | 67 | // Passkeys tells that the passkey routes exist: the header then links to | |
| 68 | 68 | // them. | |
| 69 | 69 | Passkeys bool | |
| 70 | + | // Lazy loads the loader of the collapsed and lazy files of a diff | |
| 71 | + | // (lazy.js): the commit, compare and merge request pages. | |
| 72 | + | Lazy bool | |
| 70 | 73 | // MR loads the script of the merge request page (mr.js). | |
| 71 | 74 | MR bool | |
| 72 | 75 | // IssueForm loads the script of the public issue form (issue-form.js). | |
| @@ -67,6 +67,9 @@ type Layout struct { | |||||
| 67 | // Passkeys tells that the passkey routes exist: the header then links to | 67 | // Passkeys tells that the passkey routes exist: the header then links to | ||
| 68 | // them. | 68 | // them. | ||
| 69 | Passkeys bool | 69 | Passkeys bool | ||
| 70 | + | // Lazy loads the loader of the collapsed and lazy files of a diff | |||
| 71 | + | // (lazy.js): the commit, compare and merge request pages. | |||
| 72 | + | Lazy bool | |||
| 70 | // MR loads the script of the merge request page (mr.js). | 73 | // MR loads the script of the merge request page (mr.js). | ||
| 71 | MR bool | 74 | MR bool | ||
| 72 | // IssueForm loads the script of the public issue form (issue-form.js). | 75 | // IssueForm loads the script of the public issue form (issue-form.js). | ||
M internal/adapters/web/web.go +1 -0
| @@ -207,6 +207,7 @@ func (s *Server) routes() *http.ServeMux { | |||
| 207 | 207 | mux.HandleFunc("GET /{repo}/commits", s.page(s.commits)) | |
| 208 | 208 | mux.HandleFunc("GET /{repo}/commits/{rest...}", s.page(s.commits)) | |
| 209 | 209 | mux.HandleFunc("GET /{repo}/commit/{sha}", s.page(s.commit)) | |
| 210 | + | mux.HandleFunc("GET /{repo}/commit/{sha}/files", s.page(s.commitFiles)) | |
| 210 | 211 | mux.HandleFunc("GET /{repo}/tree", s.page(s.tree)) | |
| 211 | 212 | mux.HandleFunc("GET /{repo}/tree/{rest...}", s.page(s.tree)) | |
| 212 | 213 | mux.HandleFunc("GET /{repo}/blob/{rest...}", s.page(s.blob)) | |
| @@ -207,6 +207,7 @@ func (s *Server) routes() *http.ServeMux { | |||||
| 207 | mux.HandleFunc("GET /{repo}/commits", s.page(s.commits)) | 207 | mux.HandleFunc("GET /{repo}/commits", s.page(s.commits)) | ||
| 208 | mux.HandleFunc("GET /{repo}/commits/{rest...}", s.page(s.commits)) | 208 | mux.HandleFunc("GET /{repo}/commits/{rest...}", s.page(s.commits)) | ||
| 209 | mux.HandleFunc("GET /{repo}/commit/{sha}", s.page(s.commit)) | 209 | mux.HandleFunc("GET /{repo}/commit/{sha}", s.page(s.commit)) | ||
| 210 | + | mux.HandleFunc("GET /{repo}/commit/{sha}/files", s.page(s.commitFiles)) | |||
| 210 | mux.HandleFunc("GET /{repo}/tree", s.page(s.tree)) | 211 | mux.HandleFunc("GET /{repo}/tree", s.page(s.tree)) | ||
| 211 | mux.HandleFunc("GET /{repo}/tree/{rest...}", s.page(s.tree)) | 212 | mux.HandleFunc("GET /{repo}/tree/{rest...}", s.page(s.tree)) | ||
| 212 | mux.HandleFunc("GET /{repo}/blob/{rest...}", s.page(s.blob)) | 213 | mux.HandleFunc("GET /{repo}/blob/{rest...}", s.page(s.blob)) | ||
M internal/app/testdata/web/commit-big.html +6 -3
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -141,7 +143,6 @@ | |||
| 141 | 143 | </div> | |
| 142 | 144 | </details> | |
| 143 | 145 | </div> | |
| 144 | - | <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p> | |
| 145 | 146 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | |
| 146 | 147 | <details class="dfile lnw-2" id="f1" data-file="big.txt"> | |
| 147 | 148 | <summary> | |
| @@ -149,9 +150,11 @@ | |||
| 149 | 150 | <span class="p">big.txt</span> | |
| 150 | 151 | <span class="plus">+2600</span> | |
| 151 | 152 | <span class="minus">-0</span> | |
| 152 | - | <span class="note">diff too large to show (2600 lines changed): collapsed</span> | |
| 153 | + | <span class="note">large diff (2600 lines changed): collapsed</span> | |
| 153 | 154 | </summary> | |
| 154 | - | <p class="note pad">diff too large to show (2600 lines changed): collapsed</p> | |
| 155 | + | <div class="dlazy" data-src="/diffs/commit/SHA40/files?one=1&from=0" data-manual> | |
| 156 | + | <a href="?file=0#f1">load diff</a> | |
| 157 | + | </div> | |
| 155 | 158 | </details> | |
| 156 | 159 | </div> | |
| 157 | 160 | </main> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
| @@ -141,7 +143,6 @@ | |||||
| 141 | </div> | 143 | </div> | ||
| 142 | </details> | 144 | </details> | ||
| 143 | </div> | 145 | </div> | ||
| 144 | - | <p class="notice">this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.</p> | |||
| 145 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | 146 | <div id="diffs" data-diff="unified" hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff"> | ||
| 146 | <details class="dfile lnw-2" id="f1" data-file="big.txt"> | 147 | <details class="dfile lnw-2" id="f1" data-file="big.txt"> | ||
| 147 | <summary> | 148 | <summary> | ||
| @@ -149,9 +150,11 @@ | |||||
| 149 | <span class="p">big.txt</span> | 150 | <span class="p">big.txt</span> | ||
| 150 | <span class="plus">+2600</span> | 151 | <span class="plus">+2600</span> | ||
| 151 | <span class="minus">-0</span> | 152 | <span class="minus">-0</span> | ||
| 152 | - | <span class="note">diff too large to show (2600 lines changed): collapsed</span> | 153 | + | <span class="note">large diff (2600 lines changed): collapsed</span> |
| 153 | </summary> | 154 | </summary> | ||
| 154 | - | <p class="note pad">diff too large to show (2600 lines changed): collapsed</p> | 155 | + | <div class="dlazy" data-src="/diffs/commit/SHA40/files?one=1&from=0" data-manual> |
| 156 | + | <a href="?file=0#f1">load diff</a> | |||
| 157 | + | </div> | |||
| 155 | </details> | 158 | </details> | ||
| 156 | </div> | 159 | </div> | ||
| 157 | </main> | 160 | </main> | ||
M internal/app/testdata/web/commit-binary.html +2 -0
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-merge.html +2 -0
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-modify-split.html +2 -0
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-modify.html +2 -0
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-rename.html +2 -0
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-root.html +2 -0
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/testdata/web/commit-trojan.html +2 -0
| @@ -30,6 +30,8 @@ | |||
| 30 | 30 | </script> | |
| 31 | 31 | <script src="/-/static/js/log.HASH.js" defer> | |
| 32 | 32 | </script> | |
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |
| 34 | + | </script> | |
| 33 | 35 | </head> | |
| 34 | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | |
| 35 | 37 | <a class="skip" href="#main">skip to content</a> | |
| @@ -30,6 +30,8 @@ | |||||
| 30 | </script> | 30 | </script> | ||
| 31 | <script src="/-/static/js/log.HASH.js" defer> | 31 | <script src="/-/static/js/log.HASH.js" defer> | ||
| 32 | </script> | 32 | </script> | ||
| 33 | + | <script src="/-/static/js/lazy.HASH.js" defer> | |||
| 34 | + | </script> | |||
| 33 | </head> | 35 | </head> | ||
| 34 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | 36 | <body data-ref="" data-sha="SHA40" data-sel="" data-nav="" data-paths="/diffs/paths/SHA40" data-blob="/diffs/blob/SHA40"> | ||
| 35 | <a class="skip" href="#main">skip to content</a> | 37 | <a class="skip" href="#main">skip to content</a> | ||
M internal/app/web_commit_test.go +35 -5
| @@ -112,12 +112,42 @@ func TestWebCommit(t *testing.T) { | |||
| 112 | 112 | assert.Contains(t, body, "holds invisible characters") | |
| 113 | 113 | }) | |
| 114 | 114 | ||
| 115 | - | t.Run("truncated", func(t *testing.T) { | |
| 115 | + | t.Run("large file is collapsed and loads on its own", func(t *testing.T) { | |
| 116 | 116 | body := get("/diffs/commit/"+f.big, 200) | |
| 117 | - | assert.Contains(t, body, "diff too large to show (2600 lines changed): collapsed") | |
| 118 | - | assert.Contains(t, body, "this diff is incomplete") | |
| 119 | - | assert.NotRegexp(t, `<details class="dfile" id="f1"[^>]* open`, body, "a truncated file is collapsed") | |
| 120 | - | assert.NotContains(t, body, "line 2000", "no lines of a truncated file") | |
| 117 | + | assert.Contains(t, body, "large diff (2600 lines changed): collapsed") | |
| 118 | + | assert.NotContains(t, body, "this diff is incomplete", "a collapsed file is not a global note") | |
| 119 | + | assert.NotRegexp(t, `<details class="dfile[^"]*" id="f1"[^>]* open`, body, "a collapsed file is closed") | |
| 120 | + | assert.Contains(t, body, `<div class="dlazy" data-src="/diffs/commit/`+f.big+`/files?one=1&from=0" data-manual>`) | |
| 121 | + | assert.Contains(t, body, `<a href="?file=0#f1">load diff</a>`) | |
| 122 | + | assert.NotContains(t, body, "line 2000", "no rows for a collapsed file") | |
| 123 | + | ||
| 124 | + | // One file, in full, over the 2,000 lines of the page. | |
| 125 | + | frag := get("/diffs/commit/"+f.big+"/files?one=1&from=0", 200) | |
| 126 | + | assert.NotContains(t, frag, "<html") | |
| 127 | + | assert.Equal(t, 1, strings.Count(frag, `<details class="dfile`)) | |
| 128 | + | assert.Contains(t, frag, "line 2599") | |
| 129 | + | assert.NotContains(t, frag, "data-manual") | |
| 130 | + | assert.Contains(t, frag, `id="f1"`) | |
| 131 | + | both := get("/diffs/commit/"+f.big+"/files?one=1&from=0&view=split&layouts=one", 200) | |
| 132 | + | assert.NotContains(t, both, `class="dt uni"`) | |
| 133 | + | assert.Contains(t, both, `class="dt spl"`) | |
| 134 | + | ||
| 135 | + | // Without scripts: the page splices the file in. | |
| 136 | + | page := get("/diffs/commit/"+f.big+"?file=0", 200) | |
| 137 | + | assert.Contains(t, page, "line 2599") | |
| 138 | + | assert.NotContains(t, page, "data-manual") | |
| 139 | + | assert.Contains(t, get("/diffs/commit/"+f.big+"?file=9", 200), "data-manual", "out of range: ignored") | |
| 140 | + | }) | |
| 141 | + | ||
| 142 | + | t.Run("one file route", func(t *testing.T) { | |
| 143 | + | for _, p := range []string{ | |
| 144 | + | "/files?one=1&from=1", "/files?one=1&from=-1", "/files?one=1&from=x", "/files?from=0", "/files?one=1", | |
| 145 | + | } { | |
| 146 | + | st, _, _ := h.get("/diffs/commit/" + f.big + p) | |
| 147 | + | assert.Equal(t, 404, st, p) | |
| 148 | + | } | |
| 149 | + | st, _, _ := h.get("/diffs/commit/main/files?one=1&from=0") | |
| 150 | + | assert.Equal(t, 404, st) | |
| 121 | 151 | }) | |
| 122 | 152 | ||
| 123 | 153 | t.Run("not found", func(t *testing.T) { | |
| @@ -112,12 +112,42 @@ func TestWebCommit(t *testing.T) { | |||||
| 112 | assert.Contains(t, body, "holds invisible characters") | 112 | assert.Contains(t, body, "holds invisible characters") | ||
| 113 | }) | 113 | }) | ||
| 114 | 114 | ||||
| 115 | - | t.Run("truncated", func(t *testing.T) { | 115 | + | t.Run("large file is collapsed and loads on its own", func(t *testing.T) { |
| 116 | body := get("/diffs/commit/"+f.big, 200) | 116 | body := get("/diffs/commit/"+f.big, 200) | ||
| 117 | - | assert.Contains(t, body, "diff too large to show (2600 lines changed): collapsed") | 117 | + | assert.Contains(t, body, "large diff (2600 lines changed): collapsed") |
| 118 | - | assert.Contains(t, body, "this diff is incomplete") | 118 | + | assert.NotContains(t, body, "this diff is incomplete", "a collapsed file is not a global note") |
| 119 | - | assert.NotRegexp(t, `<details class="dfile" id="f1"[^>]* open`, body, "a truncated file is collapsed") | 119 | + | assert.NotRegexp(t, `<details class="dfile[^"]*" id="f1"[^>]* open`, body, "a collapsed file is closed") |
| 120 | - | assert.NotContains(t, body, "line 2000", "no lines of a truncated file") | 120 | + | assert.Contains(t, body, `<div class="dlazy" data-src="/diffs/commit/`+f.big+`/files?one=1&from=0" data-manual>`) |
| 121 | + | assert.Contains(t, body, `<a href="?file=0#f1">load diff</a>`) | |||
| 122 | + | assert.NotContains(t, body, "line 2000", "no rows for a collapsed file") | |||
| 123 | + | ||||
| 124 | + | // One file, in full, over the 2,000 lines of the page. | |||
| 125 | + | frag := get("/diffs/commit/"+f.big+"/files?one=1&from=0", 200) | |||
| 126 | + | assert.NotContains(t, frag, "<html") | |||
| 127 | + | assert.Equal(t, 1, strings.Count(frag, `<details class="dfile`)) | |||
| 128 | + | assert.Contains(t, frag, "line 2599") | |||
| 129 | + | assert.NotContains(t, frag, "data-manual") | |||
| 130 | + | assert.Contains(t, frag, `id="f1"`) | |||
| 131 | + | both := get("/diffs/commit/"+f.big+"/files?one=1&from=0&view=split&layouts=one", 200) | |||
| 132 | + | assert.NotContains(t, both, `class="dt uni"`) | |||
| 133 | + | assert.Contains(t, both, `class="dt spl"`) | |||
| 134 | + | ||||
| 135 | + | // Without scripts: the page splices the file in. | |||
| 136 | + | page := get("/diffs/commit/"+f.big+"?file=0", 200) | |||
| 137 | + | assert.Contains(t, page, "line 2599") | |||
| 138 | + | assert.NotContains(t, page, "data-manual") | |||
| 139 | + | assert.Contains(t, get("/diffs/commit/"+f.big+"?file=9", 200), "data-manual", "out of range: ignored") | |||
| 140 | + | }) | |||
| 141 | + | ||||
| 142 | + | t.Run("one file route", func(t *testing.T) { | |||
| 143 | + | for _, p := range []string{ | |||
| 144 | + | "/files?one=1&from=1", "/files?one=1&from=-1", "/files?one=1&from=x", "/files?from=0", "/files?one=1", | |||
| 145 | + | } { | |||
| 146 | + | st, _, _ := h.get("/diffs/commit/" + f.big + p) | |||
| 147 | + | assert.Equal(t, 404, st, p) | |||
| 148 | + | } | |||
| 149 | + | st, _, _ := h.get("/diffs/commit/main/files?one=1&from=0") | |||
| 150 | + | assert.Equal(t, 404, st) | |||
| 121 | }) | 151 | }) | ||
| 122 | 152 | ||||
| 123 | t.Run("not found", func(t *testing.T) { | 153 | t.Run("not found", func(t *testing.T) { | ||
M internal/app/web_mr_test.go +49 -0
| @@ -921,3 +921,52 @@ func TestWebMergeRequestMergeabilityLive(t *testing.T) { | |||
| 921 | 921 | break | |
| 922 | 922 | } | |
| 923 | 923 | } | |
| 924 | + | ||
| 925 | + | // Collapsed files of a merge request: a generated file and a file over the | |
| 926 | + | // line cap are listed closed, with no rows and a manual placeholder; /files | |
| 927 | + | // with one=1 reads exactly one file in full, whatever its index; the page | |
| 928 | + | // without scripts (?file=i) splices the file in. | |
| 929 | + | func TestWebMergeRequestCollapsedFiles(t *testing.T) { | |
| 930 | + | r := newMRRepo(t, domain.VisibilityPublic) | |
| 931 | + | b := browser{r.harness} | |
| 932 | + | var big strings.Builder | |
| 933 | + | for i := range 2600 { | |
| 934 | + | fmt.Fprintf(&big, "row %d\n", i) | |
| 935 | + | } | |
| 936 | + | r.writeFile(r.wt, "zz/big.txt", []byte(big.String())) | |
| 937 | + | r.writeFile(r.wt, "vendor/lib/gen.txt", []byte("generated line\n")) | |
| 938 | + | r.writeFile(r.wt, "zz/small.txt", []byte("small line\n")) | |
| 939 | + | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | |
| 940 | + | r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Collapsed").ok(t) | |
| 941 | + | ||
| 942 | + | page := b.get("/proj/merge-requests/1", nil) | |
| 943 | + | require.Equal(t, 200, page.status) | |
| 944 | + | assert.Equal(t, 2, strings.Count(page.body, "data-manual"), "the large and the generated file") | |
| 945 | + | assert.Contains(t, page.body, "generated file: collapsed") | |
| 946 | + | assert.Contains(t, page.body, "large diff (2600 lines changed): collapsed") | |
| 947 | + | assert.NotContains(t, page.body, "row 5<", "no rows for a collapsed file") | |
| 948 | + | assert.Contains(t, page.body, "small line") | |
| 949 | + | assert.NotContains(t, page.body, "this diff is incomplete") | |
| 950 | + | srcs := regexp.MustCompile(`data-src="([^"]*one=1&from=(\d+))" data-manual`).FindAllStringSubmatch(page.body, -1) | |
| 951 | + | require.Len(t, srcs, 2) | |
| 952 | + | ||
| 953 | + | for _, m := range srcs { | |
| 954 | + | one := b.get(html.UnescapeString(m[1]), nil) | |
| 955 | + | require.Equal(t, 200, one.status, one.body) | |
| 956 | + | assert.Equal(t, 1, strings.Count(one.body, `<details class="dfile`), "exactly one file") | |
| 957 | + | assert.NotContains(t, one.body, "data-manual") | |
| 958 | + | } | |
| 959 | + | bigOne := b.get(html.UnescapeString(srcs[1][1]), nil) | |
| 960 | + | genOne := b.get(html.UnescapeString(srcs[0][1]), nil) | |
| 961 | + | assert.True(t, strings.Contains(bigOne.body, "row 2599") != strings.Contains(genOne.body, "row 2599")) | |
| 962 | + | assert.Contains(t, bigOne.body+genOne.body, "generated line") | |
| 963 | + | ||
| 964 | + | assert.Equal(t, 404, b.get(strings.Replace(html.UnescapeString(srcs[0][1]), "from="+srcs[0][2], "from=999", 1), nil).status) | |
| 965 | + | assert.Equal(t, 404, b.get(strings.Replace(html.UnescapeString(srcs[0][1]), "from="+srcs[0][2], "from=-1", 1), nil).status) | |
| 966 | + | ||
| 967 | + | for _, m := range srcs { | |
| 968 | + | own := b.get("/proj/merge-requests/1?file="+m[2], nil) | |
| 969 | + | require.Equal(t, 200, own.status) | |
| 970 | + | assert.Equal(t, 1, strings.Count(own.body, "data-manual"), "that one file is spliced in") | |
| 971 | + | } | |
| 972 | + | } | |
| @@ -921,3 +921,52 @@ func TestWebMergeRequestMergeabilityLive(t *testing.T) { | |||||
| 921 | break | 921 | break | ||
| 922 | } | 922 | } | ||
| 923 | } | 923 | } | ||
| 924 | + | ||||
| 925 | + | // Collapsed files of a merge request: a generated file and a file over the | |||
| 926 | + | // line cap are listed closed, with no rows and a manual placeholder; /files | |||
| 927 | + | // with one=1 reads exactly one file in full, whatever its index; the page | |||
| 928 | + | // without scripts (?file=i) splices the file in. | |||
| 929 | + | func TestWebMergeRequestCollapsedFiles(t *testing.T) { | |||
| 930 | + | r := newMRRepo(t, domain.VisibilityPublic) | |||
| 931 | + | b := browser{r.harness} | |||
| 932 | + | var big strings.Builder | |||
| 933 | + | for i := range 2600 { | |||
| 934 | + | fmt.Fprintf(&big, "row %d\n", i) | |||
| 935 | + | } | |||
| 936 | + | r.writeFile(r.wt, "zz/big.txt", []byte(big.String())) | |||
| 937 | + | r.writeFile(r.wt, "vendor/lib/gen.txt", []byte("generated line\n")) | |||
| 938 | + | r.writeFile(r.wt, "zz/small.txt", []byte("small line\n")) | |||
| 939 | + | r.mustGit(r.wt, "push", "-q", r.url, "feat/search") | |||
| 940 | + | r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Collapsed").ok(t) | |||
| 941 | + | ||||
| 942 | + | page := b.get("/proj/merge-requests/1", nil) | |||
| 943 | + | require.Equal(t, 200, page.status) | |||
| 944 | + | assert.Equal(t, 2, strings.Count(page.body, "data-manual"), "the large and the generated file") | |||
| 945 | + | assert.Contains(t, page.body, "generated file: collapsed") | |||
| 946 | + | assert.Contains(t, page.body, "large diff (2600 lines changed): collapsed") | |||
| 947 | + | assert.NotContains(t, page.body, "row 5<", "no rows for a collapsed file") | |||
| 948 | + | assert.Contains(t, page.body, "small line") | |||
| 949 | + | assert.NotContains(t, page.body, "this diff is incomplete") | |||
| 950 | + | srcs := regexp.MustCompile(`data-src="([^"]*one=1&from=(\d+))" data-manual`).FindAllStringSubmatch(page.body, -1) | |||
| 951 | + | require.Len(t, srcs, 2) | |||
| 952 | + | ||||
| 953 | + | for _, m := range srcs { | |||
| 954 | + | one := b.get(html.UnescapeString(m[1]), nil) | |||
| 955 | + | require.Equal(t, 200, one.status, one.body) | |||
| 956 | + | assert.Equal(t, 1, strings.Count(one.body, `<details class="dfile`), "exactly one file") | |||
| 957 | + | assert.NotContains(t, one.body, "data-manual") | |||
| 958 | + | } | |||
| 959 | + | bigOne := b.get(html.UnescapeString(srcs[1][1]), nil) | |||
| 960 | + | genOne := b.get(html.UnescapeString(srcs[0][1]), nil) | |||
| 961 | + | assert.True(t, strings.Contains(bigOne.body, "row 2599") != strings.Contains(genOne.body, "row 2599")) | |||
| 962 | + | assert.Contains(t, bigOne.body+genOne.body, "generated line") | |||
| 963 | + | ||||
| 964 | + | assert.Equal(t, 404, b.get(strings.Replace(html.UnescapeString(srcs[0][1]), "from="+srcs[0][2], "from=999", 1), nil).status) | |||
| 965 | + | assert.Equal(t, 404, b.get(strings.Replace(html.UnescapeString(srcs[0][1]), "from="+srcs[0][2], "from=-1", 1), nil).status) | |||
| 966 | + | ||||
| 967 | + | for _, m := range srcs { | |||
| 968 | + | own := b.get("/proj/merge-requests/1?file="+m[2], nil) | |||
| 969 | + | require.Equal(t, 200, own.status) | |||
| 970 | + | assert.Equal(t, 1, strings.Count(own.body, "data-manual"), "that one file is spliced in") | |||
| 971 | + | } | |||
| 972 | + | } | |||