detached e9c6360

#29 feat(web): merge merge requests asynchronously with htmx

state
merged · cf26365 · squash
branch
feat/mr-async-merge → main
opened
bnema ·
updated

The merge request Merge action now runs through htmx for every mode (fast-forward, squash, merge) instead of a full-page POST and 303 reload.

  • The form posts asynchronously, drops double submits and shows a pending indicator; without JS it still posts normally.
  • htmx merges get OOB fragments (state badge + action area). Refusals, failures, expired sessions and rejected origins render a status in the merge box.
  • Other open tabs of the same MR receive the merged state through the live stream.
  • The checks-label observer watches the stable MR bar so it survives swaps.

Deferred: the fragment path still builds the full page view (diff, comments) before rendering only state/actions; a dedicated state view builder is a follow-up.

Checks: go test ./..., go vet, node jstest suite (198 pass). Not manually tested in a browser.

CommitSubjectAuthorDateSignatureStatus
e9c6360fix(web): show session refusals in the async merge box and keep a single merge error alertbriceGPG Verified
5fea81ffeat(web): merge merge requests asynchronously with htmx and live state updatesbriceGPG Verified

Checks 4

64 files · +1924 -2040
4/4
M docs/issues.md +1 -3
@@ -50,9 +50,7 @@ Limits: the 100 most recent commits of the merge request, 16 KiB of each text, 1
5050
5151 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
5252
53-The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; lowercase `i` still opens the issues tab.
54-
55-The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
53+The page updates live: merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
5654
5755 ## Public form and moderation
5856
@@ -50,9 +50,7 @@ Limits: the 100 most recent commits of the merge request, 16 KiB of each text, 1
50 50
51 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.51 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
52 52
53-The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; lowercase `i` still opens the issues tab.53+The page updates live: merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
54-
55-The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
56 54
57 ## Public form and moderation55 ## Public form and moderation
58 56
M docs/releases.md +5 -44
@@ -57,60 +57,21 @@ the S3 endpoint. Stored-token publishers need `release:write` and, for private
5757 repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by
5858 default).
5959
60-GoReleaser does not read `GITHUB_API_URL` or `GITHUB_SERVER_URL` by itself, so
61-template `github_urls` from them (replace the fallback host and repository):
60+GoReleaser configuration (replace the host and repository):
6261
6362 ```yaml
6463 version: 2
65-# In CI the runner sets GITHUB_API_URL and GITHUB_SERVER_URL to the address the
66-# job can reach; the public URLs are the fallback for a local run.
6764 github_urls:
68- api: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/'
69- upload: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/uploads/'
70- download: '{{ or (index .Env "GITHUB_SERVER_URL") "https://git.example.org" }}/'
65+ api: https://git.example.org/-/github/api/
66+ upload: https://git.example.org/-/github/api/uploads/
67+ download: https://git.example.org/
7168 release:
7269 github:
7370 owner: owner
7471 name: app
7572 ```
7673
77-In a CI job the runner rewrites the server and API URLs to an origin the job
78-can reach, which may differ from the public URL (for example `http://ermite:7341`
79-with a relayed forge). Sandboxes block private and CGNAT ranges, so a job that
80-targets only the public URL times out (`dial tcp 100.x:443: i/o timeout`). A
81-plain-http relay needs no TLS option. The API base gets `api/v3/` appended by
82-the client; ermite accepts that alias. Locally, run
83-`GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout; the
84-fallbacks apply.
85-
86-Workflow:
87-
88-```yaml
89-name: release
90-on:
91- push:
92- tags: ['v*']
93-permissions:
94- contents: write
95-jobs:
96- release:
97- runs-on: ubuntu-latest
98- steps:
99- - uses: actions/checkout@v4
100- with:
101- fetch-depth: 0
102- - uses: actions/setup-go@v5
103- with:
104- go-version-file: go.mod
105- - uses: goreleaser/goreleaser-action@v6
106- with:
107- args: release --clean
108- env:
109- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110-```
111-
112-An existing GitHub workflow must be enabled on ermite:
113-`ermite workflow enable <repo> .github/workflows/release.yml`.
74+Run `GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout.
11475
11576 On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and
11677 `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release:
@@ -57,60 +57,21 @@ the S3 endpoint. Stored-token publishers need `release:write` and, for private
57 repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by57 repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by
58 default).58 default).
59 59
60-GoReleaser does not read `GITHUB_API_URL` or `GITHUB_SERVER_URL` by itself, so60+GoReleaser configuration (replace the host and repository):
61-template `github_urls` from them (replace the fallback host and repository):
62 61
63 ```yaml62 ```yaml
64 version: 263 version: 2
65-# In CI the runner sets GITHUB_API_URL and GITHUB_SERVER_URL to the address the
66-# job can reach; the public URLs are the fallback for a local run.
67 github_urls:64 github_urls:
68- api: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/'65+ api: https://git.example.org/-/github/api/
69- upload: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/uploads/'66+ upload: https://git.example.org/-/github/api/uploads/
70- download: '{{ or (index .Env "GITHUB_SERVER_URL") "https://git.example.org" }}/'67+ download: https://git.example.org/
71 release:68 release:
72 github:69 github:
73 owner: owner70 owner: owner
74 name: app71 name: app
75 ```72 ```
76 73
77-In a CI job the runner rewrites the server and API URLs to an origin the job74+Run `GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout.
78-can reach, which may differ from the public URL (for example `http://ermite:7341`
79-with a relayed forge). Sandboxes block private and CGNAT ranges, so a job that
80-targets only the public URL times out (`dial tcp 100.x:443: i/o timeout`). A
81-plain-http relay needs no TLS option. The API base gets `api/v3/` appended by
82-the client; ermite accepts that alias. Locally, run
83-`GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout; the
84-fallbacks apply.
85-
86-Workflow:
87-
88-```yaml
89-name: release
90-on:
91- push:
92- tags: ['v*']
93-permissions:
94- contents: write
95-jobs:
96- release:
97- runs-on: ubuntu-latest
98- steps:
99- - uses: actions/checkout@v4
100- with:
101- fetch-depth: 0
102- - uses: actions/setup-go@v5
103- with:
104- go-version-file: go.mod
105- - uses: goreleaser/goreleaser-action@v6
106- with:
107- args: release --clean
108- env:
109- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110-```
111-
112-An existing GitHub workflow must be enabled on ermite:
113-`ermite workflow enable <repo> .github/workflows/release.yml`.
114 75
115 On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and76 On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and
116 `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release:77 `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release:
M go.mod +5 -5
@@ -1,6 +1,6 @@
11 module bnema.dev/ermite
22
3-go 1.27
3+go 1.27.1
44
55 tool (
66 github.com/a-h/templ/cmd/templ
@@ -12,10 +12,10 @@ tool (
1212 )
1313
1414 require (
15- bnema.dev/ermite-actions v0.2.1
16- bnema.dev/ermite-api v0.16.1
17- bnema.dev/ermite-git v0.1.1
18- bnema.dev/ermite-kv v0.1.1
15+ bnema.dev/ermite-actions v0.2.0
16+ bnema.dev/ermite-api v0.16.0
17+ bnema.dev/ermite-git v0.1.0
18+ bnema.dev/ermite-kv v0.1.0
1919 connectrpc.com/connect v1.21.0
2020 github.com/ProtonMail/go-crypto v1.4.1
2121 github.com/a-h/templ v0.3.1020
@@ -1,6 +1,6 @@
1 module bnema.dev/ermite1 module bnema.dev/ermite
2 2
3-go 1.273+go 1.27.1
4 4
5 tool (5 tool (
6 github.com/a-h/templ/cmd/templ6 github.com/a-h/templ/cmd/templ
@@ -12,10 +12,10 @@ tool (
12 )12 )
13 13
14 require (14 require (
15- bnema.dev/ermite-actions v0.2.115+ bnema.dev/ermite-actions v0.2.0
16- bnema.dev/ermite-api v0.16.116+ bnema.dev/ermite-api v0.16.0
17- bnema.dev/ermite-git v0.1.117+ bnema.dev/ermite-git v0.1.0
18- bnema.dev/ermite-kv v0.1.118+ bnema.dev/ermite-kv v0.1.0
19 connectrpc.com/connect v1.21.019 connectrpc.com/connect v1.21.0
20 github.com/ProtonMail/go-crypto v1.4.120 github.com/ProtonMail/go-crypto v1.4.1
21 github.com/a-h/templ v0.3.102021 github.com/a-h/templ v0.3.1020
M go.sum +8 -8 generated file: collapsed
M internal/adapters/web/fragments.go +15 -0
@@ -6,6 +6,7 @@ import (
66 "bytes"
77 "context"
88 "errors"
9+ "net/http"
910 "strconv"
1011 "strings"
1112 "sync"
@@ -192,6 +193,20 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
192193 case domain.MergeRequestOpened:
193194 return l.mrNotice(ctx, ev.MergeRequest.Number, "a merge request was opened — reload")
194195 case domain.MergeRequestMerged:
196+ if l.view == "mr" && strconv.FormatInt(ev.MergeRequest.Number, 10) == l.n {
197+ r, err := http.NewRequestWithContext(ctx, http.MethodGet, mrHref(l.repo, ev.MergeRequest.Number), nil)
198+ if err != nil {
199+ return nil, err
200+ }
201+ r.SetPathValue("repo", l.repo)
202+ r.SetPathValue("n", l.n)
203+ c, _, err := l.s.mrPageView(ctx, r, false, true)
204+ if err != nil {
205+ return nil, err
206+ }
207+ f, err := renderFragment(ctx, "mr-mergeability", c)
208+ return []fragment{f}, err
209+ }
195210 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was merged — reload")
196211 case domain.MergeRequestClosed:
197212 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
@@ -6,6 +6,7 @@ import (
6 "bytes"6 "bytes"
7 "context"7 "context"
8 "errors"8 "errors"
9+ "net/http"
9 "strconv"10 "strconv"
10 "strings"11 "strings"
11 "sync"12 "sync"
@@ -192,6 +193,20 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
192 case domain.MergeRequestOpened:193 case domain.MergeRequestOpened:
193 return l.mrNotice(ctx, ev.MergeRequest.Number, "a merge request was opened — reload")194 return l.mrNotice(ctx, ev.MergeRequest.Number, "a merge request was opened — reload")
194 case domain.MergeRequestMerged:195 case domain.MergeRequestMerged:
196+ if l.view == "mr" && strconv.FormatInt(ev.MergeRequest.Number, 10) == l.n {
197+ r, err := http.NewRequestWithContext(ctx, http.MethodGet, mrHref(l.repo, ev.MergeRequest.Number), nil)
198+ if err != nil {
199+ return nil, err
200+ }
201+ r.SetPathValue("repo", l.repo)
202+ r.SetPathValue("n", l.n)
203+ c, _, err := l.s.mrPageView(ctx, r, false, true)
204+ if err != nil {
205+ return nil, err
206+ }
207+ f, err := renderFragment(ctx, "mr-mergeability", c)
208+ return []fragment{f}, err
209+ }
195 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was merged — reload")210 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was merged — reload")
196 case domain.MergeRequestClosed:211 case domain.MergeRequestClosed:
197 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")212 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
M internal/adapters/web/inbox.go +3 -25
@@ -67,17 +67,8 @@ type InboxRepoLink struct {
6767 Current bool
6868 }
6969
70-// InboxRunRow adds repository and start time to the Actions run display.
71-type InboxRunRow struct {
72- RunRow
73- Repo, RepoHref, When, WhenAt string
74-}
75-
76-// InboxPage is the inbox: counters, repository filter and item lists.
70+// InboxPage is the inbox: the counters, the filter and the two lists.
7771 type InboxPage struct {
78- Running int
79- RunsEnabled bool
80- Runs []InboxRunRow
8172 // MergeRequests and Issues count every open item, whatever the filter.
8273 MergeRequests, Issues int
8374 // PendingCount counts every issue awaiting moderation.
@@ -115,19 +106,6 @@ func inboxFilter(r *http.Request) (domain.RepoName, error) {
115106 func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {
116107 now := s.cfg.Now()
117108 p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, PendingCount: ov.Counts.Pending, Filter: filter.String()}
118- p.Running, p.RunsEnabled = ov.Counts.Running, ov.RunsEnabled
119- for _, run := range ov.Runs {
120- r := InboxRunRow{RunRow: s.runRow(run.Repo.String(), run, now), Repo: run.Repo.String(), RepoHref: repoHref(run.Repo.String())}
121- if run.Status == domain.CIInProgress {
122- r.State = "in progress"
123- }
124- ts := run.StartedAt
125- if ts.IsZero() {
126- ts = run.CreatedAt
127- }
128- r.When, r.WhenAt = whenOf(now, ts)
129- p.Runs = append(p.Runs, r)
130- }
131109 for _, pi := range ov.Pending {
132110 p.Pending = append(p.Pending, s.pendingRow(now, pi, filter))
133111 }
@@ -288,7 +266,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
288266 switch we.Event.(type) {
289267 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
290268 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,
291- domain.RepoUpdated, domain.RepoDeleted, domain.RunUpdated:
269+ domain.RepoUpdated, domain.RepoDeleted:
292270 default:
293271 return nil, nil
294272 }
@@ -305,7 +283,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
305283 }
306284 p := l.s.inboxPage(ov, filter)
307285 var out []fragment
308- for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxRuns(p), InboxMRs(p), InboxIssues(p)} {
286+ for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxMRs(p), InboxIssues(p)} {
309287 f, err := renderFragment(ctx, inboxEvent, c)
310288 if err != nil {
311289 return nil, err
@@ -67,17 +67,8 @@ type InboxRepoLink struct {
67 Current bool67 Current bool
68 }68 }
69 69
70-// InboxRunRow adds repository and start time to the Actions run display.70+// InboxPage is the inbox: the counters, the filter and the two lists.
71-type InboxRunRow struct {
72- RunRow
73- Repo, RepoHref, When, WhenAt string
74-}
75-
76-// InboxPage is the inbox: counters, repository filter and item lists.
77 type InboxPage struct {71 type InboxPage struct {
78- Running int
79- RunsEnabled bool
80- Runs []InboxRunRow
81 // MergeRequests and Issues count every open item, whatever the filter.72 // MergeRequests and Issues count every open item, whatever the filter.
82 MergeRequests, Issues int73 MergeRequests, Issues int
83 // PendingCount counts every issue awaiting moderation.74 // PendingCount counts every issue awaiting moderation.
@@ -115,19 +106,6 @@ func inboxFilter(r *http.Request) (domain.RepoName, error) {
115 func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {106 func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {
116 now := s.cfg.Now()107 now := s.cfg.Now()
117 p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, PendingCount: ov.Counts.Pending, Filter: filter.String()}108 p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, PendingCount: ov.Counts.Pending, Filter: filter.String()}
118- p.Running, p.RunsEnabled = ov.Counts.Running, ov.RunsEnabled
119- for _, run := range ov.Runs {
120- r := InboxRunRow{RunRow: s.runRow(run.Repo.String(), run, now), Repo: run.Repo.String(), RepoHref: repoHref(run.Repo.String())}
121- if run.Status == domain.CIInProgress {
122- r.State = "in progress"
123- }
124- ts := run.StartedAt
125- if ts.IsZero() {
126- ts = run.CreatedAt
127- }
128- r.When, r.WhenAt = whenOf(now, ts)
129- p.Runs = append(p.Runs, r)
130- }
131 for _, pi := range ov.Pending {109 for _, pi := range ov.Pending {
132 p.Pending = append(p.Pending, s.pendingRow(now, pi, filter))110 p.Pending = append(p.Pending, s.pendingRow(now, pi, filter))
133 }111 }
@@ -288,7 +266,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
288 switch we.Event.(type) {266 switch we.Event.(type) {
289 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,267 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
290 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,268 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,
291- domain.RepoUpdated, domain.RepoDeleted, domain.RunUpdated:269+ domain.RepoUpdated, domain.RepoDeleted:
292 default:270 default:
293 return nil, nil271 return nil, nil
294 }272 }
@@ -305,7 +283,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
305 }283 }
306 p := l.s.inboxPage(ov, filter)284 p := l.s.inboxPage(ov, filter)
307 var out []fragment285 var out []fragment
308- for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxRuns(p), InboxMRs(p), InboxIssues(p)} {286+ for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxMRs(p), InboxIssues(p)} {
309 f, err := renderFragment(ctx, inboxEvent, c)287 f, err := renderFragment(ctx, inboxEvent, c)
310 if err != nil {288 if err != nil {
311 return nil, err289 return nil, err
M internal/adapters/web/inbox.templ +1 -34
@@ -12,7 +12,6 @@ templ Inbox(l Layout, p InboxPage) {
1212 @InboxCounters(p)
1313 @InboxRepos(p)
1414 @InboxPending(p)
15- @InboxRuns(p)
1615 @InboxMRs(p)
1716 @InboxIssues(p)
1817 </div>
@@ -21,7 +20,7 @@ templ Inbox(l Layout, p InboxPage) {
2120
2221 templ InboxCounters(p InboxPage) {
2322 <p id="inbox-counts" class="inbox-counts">
24- <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation · <strong>{ itoa(p.Running) }</strong> running
23+ <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation
2524 </p>
2625 }
2726
@@ -114,35 +113,3 @@ templ inboxRows(rows []InboxRow, label string) {
114113 </table>
115114 }
116115 }
117-
118-// InboxRuns stays as an empty fragment when CI is disabled.
119-templ InboxRuns(p InboxPage) {
120- <section id="inbox-runs" hidden?={ !p.RunsEnabled }
121- if p.RunsEnabled {
122- aria-labelledby="inbox-runs-h"
123- }
124- >
125- if p.RunsEnabled {
126- <h2 id="inbox-runs-h" class="rule-h"><span>Running</span><span class="count">{ itoa(len(p.Runs)) }</span></h2>
127- if len(p.Runs) == 0 {
128- <p class="lede dim">no runs in progress.</p>
129- } else {
130- <table class="list mrs" aria-label="Running">
131- <tbody data-rows>
132- for _, r := range p.Runs {
133- <tr>
134- <td class="c-st">@CheckIcon(r.Kind, r.State)<span class="dim">{ r.State }</span></td>
135- <td class="c-subject"><a href={ templ.SafeURL(r.Href) }>{ r.Workflow }{ " " }{ r.Number }</a> <a class="mute" href={ templ.SafeURL(r.RepoHref) }>{ r.Repo }</a> <span class="dim">{ r.Ref }{ " " }{ r.Event }</span></td>
136- <td class="c-when nowrap dim">
137- if r.WhenAt != "" {
138- <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
139- }
140- </td>
141- </tr>
142- }
143- </tbody>
144- </table>
145- }
146- }
147- </section>
148-}
@@ -12,7 +12,6 @@ templ Inbox(l Layout, p InboxPage) {
12 @InboxCounters(p)12 @InboxCounters(p)
13 @InboxRepos(p)13 @InboxRepos(p)
14 @InboxPending(p)14 @InboxPending(p)
15- @InboxRuns(p)
16 @InboxMRs(p)15 @InboxMRs(p)
17 @InboxIssues(p)16 @InboxIssues(p)
18 </div>17 </div>
@@ -21,7 +20,7 @@ templ Inbox(l Layout, p InboxPage) {
21 20
22 templ InboxCounters(p InboxPage) {21 templ InboxCounters(p InboxPage) {
23 <p id="inbox-counts" class="inbox-counts">22 <p id="inbox-counts" class="inbox-counts">
24- <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation · <strong>{ itoa(p.Running) }</strong> running23+ <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation
25 </p>24 </p>
26 }25 }
27 26
@@ -114,35 +113,3 @@ templ inboxRows(rows []InboxRow, label string) {
114 </table>113 </table>
115 }114 }
116 }115 }
117-
118-// InboxRuns stays as an empty fragment when CI is disabled.
119-templ InboxRuns(p InboxPage) {
120- <section id="inbox-runs" hidden?={ !p.RunsEnabled }
121- if p.RunsEnabled {
122- aria-labelledby="inbox-runs-h"
123- }
124- >
125- if p.RunsEnabled {
126- <h2 id="inbox-runs-h" class="rule-h"><span>Running</span><span class="count">{ itoa(len(p.Runs)) }</span></h2>
127- if len(p.Runs) == 0 {
128- <p class="lede dim">no runs in progress.</p>
129- } else {
130- <table class="list mrs" aria-label="Running">
131- <tbody data-rows>
132- for _, r := range p.Runs {
133- <tr>
134- <td class="c-st">@CheckIcon(r.Kind, r.State)<span class="dim">{ r.State }</span></td>
135- <td class="c-subject"><a href={ templ.SafeURL(r.Href) }>{ r.Workflow }{ " " }{ r.Number }</a> <a class="mute" href={ templ.SafeURL(r.RepoHref) }>{ r.Repo }</a> <span class="dim">{ r.Ref }{ " " }{ r.Event }</span></td>
136- <td class="c-when nowrap dim">
137- if r.WhenAt != "" {
138- <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
139- }
140- </td>
141- </tr>
142- }
143- </tbody>
144- </table>
145- }
146- }
147- </section>
148-}
M internal/adapters/web/inbox_templ.go +173 -451
@@ -62,10 +62,6 @@ func Inbox(l Layout, p InboxPage) templ.Component {
6262 if templ_7745c5c3_Err != nil {
6363 return templ_7745c5c3_Err
6464 }
65- templ_7745c5c3_Err = InboxRuns(p).Render(ctx, templ_7745c5c3_Buffer)
66- if templ_7745c5c3_Err != nil {
67- return templ_7745c5c3_Err
68- }
6965 templ_7745c5c3_Err = InboxMRs(p).Render(ctx, templ_7745c5c3_Buffer)
7066 if templ_7745c5c3_Err != nil {
7167 return templ_7745c5c3_Err
@@ -116,7 +112,7 @@ func InboxCounters(p InboxPage) templ.Component {
116112 var templ_7745c5c3_Var4 string
117113 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.MergeRequests))
118114 if templ_7745c5c3_Err != nil {
119- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 33}
115+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 23, Col: 33}
120116 }
121117 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
122118 if templ_7745c5c3_Err != nil {
@@ -129,7 +125,7 @@ func InboxCounters(p InboxPage) templ.Component {
129125 var templ_7745c5c3_Var5 string
130126 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Issues))
131127 if templ_7745c5c3_Err != nil {
132- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 87}
128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 23, Col: 87}
133129 }
134130 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
135131 if templ_7745c5c3_Err != nil {
@@ -142,26 +138,13 @@ func InboxCounters(p InboxPage) templ.Component {
142138 var templ_7745c5c3_Var6 string
143139 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.PendingCount))
144140 if templ_7745c5c3_Err != nil {
145- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 139}
141+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 23, Col: 139}
146142 }
147143 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
148144 if templ_7745c5c3_Err != nil {
149145 return templ_7745c5c3_Err
150146 }
151- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</strong> awaiting moderation · <strong>")
152- if templ_7745c5c3_Err != nil {
153- return templ_7745c5c3_Err
154- }
155- var templ_7745c5c3_Var7 string
156- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Running))
157- if templ_7745c5c3_Err != nil {
158- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 199}
159- }
160- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
161- if templ_7745c5c3_Err != nil {
162- return templ_7745c5c3_Err
163- }
164- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</strong> running</p>")
147+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</strong> awaiting moderation</p>")
165148 if templ_7745c5c3_Err != nil {
166149 return templ_7745c5c3_Err
167150 }
@@ -185,58 +168,58 @@ func InboxRepos(p InboxPage) templ.Component {
185168 }()
186169 }
187170 ctx = templ.InitializeContext(ctx)
188- templ_7745c5c3_Var8 := templ.GetChildren(ctx)
189- if templ_7745c5c3_Var8 == nil {
190- templ_7745c5c3_Var8 = templ.NopComponent
171+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
172+ if templ_7745c5c3_Var7 == nil {
173+ templ_7745c5c3_Var7 = templ.NopComponent
191174 }
192175 ctx = templ.ClearChildren(ctx)
193- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<nav id=\"inbox-repos\" class=\"since\" aria-label=\"Repository\">")
176+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<nav id=\"inbox-repos\" class=\"since\" aria-label=\"Repository\">")
194177 if templ_7745c5c3_Err != nil {
195178 return templ_7745c5c3_Err
196179 }
197180 for _, r := range p.Repos {
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
199182 if templ_7745c5c3_Err != nil {
200183 return templ_7745c5c3_Err
201184 }
202- var templ_7745c5c3_Var9 templ.SafeURL
203- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
185+ var templ_7745c5c3_Var8 templ.SafeURL
186+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
204187 if templ_7745c5c3_Err != nil {
205- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 32, Col: 32}
188+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 31, Col: 32}
206189 }
207- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
190+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
208191 if templ_7745c5c3_Err != nil {
209192 return templ_7745c5c3_Err
210193 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"")
194+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"")
212195 if templ_7745c5c3_Err != nil {
213196 return templ_7745c5c3_Err
214197 }
215198 if r.Current {
216- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " aria-current=\"page\"")
199+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " aria-current=\"page\"")
217200 if templ_7745c5c3_Err != nil {
218201 return templ_7745c5c3_Err
219202 }
220203 }
221- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ">")
204+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">")
222205 if templ_7745c5c3_Err != nil {
223206 return templ_7745c5c3_Err
224207 }
225- var templ_7745c5c3_Var10 string
226- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(r.Label)
208+ var templ_7745c5c3_Var9 string
209+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(r.Label)
227210 if templ_7745c5c3_Err != nil {
228- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 36, Col: 13}
211+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 35, Col: 13}
229212 }
230- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
213+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
231214 if templ_7745c5c3_Err != nil {
232215 return templ_7745c5c3_Err
233216 }
234- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</a>")
217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</a>")
235218 if templ_7745c5c3_Err != nil {
236219 return templ_7745c5c3_Err
237220 }
238221 }
239- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</nav>")
222+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</nav>")
240223 if templ_7745c5c3_Err != nil {
241224 return templ_7745c5c3_Err
242225 }
@@ -263,219 +246,219 @@ func InboxPending(p InboxPage) templ.Component {
263246 }()
264247 }
265248 ctx = templ.InitializeContext(ctx)
266- templ_7745c5c3_Var11 := templ.GetChildren(ctx)
267- if templ_7745c5c3_Var11 == nil {
268- templ_7745c5c3_Var11 = templ.NopComponent
249+ templ_7745c5c3_Var10 := templ.GetChildren(ctx)
250+ if templ_7745c5c3_Var10 == nil {
251+ templ_7745c5c3_Var10 = templ.NopComponent
269252 }
270253 ctx = templ.ClearChildren(ctx)
271- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<section id=\"inbox-pending\" aria-labelledby=\"inbox-pending-h\"><h2 id=\"inbox-pending-h\" class=\"rule-h\"><span>Awaiting moderation</span><span class=\"count\">")
254+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<section id=\"inbox-pending\" aria-labelledby=\"inbox-pending-h\"><h2 id=\"inbox-pending-h\" class=\"rule-h\"><span>Awaiting moderation</span><span class=\"count\">")
272255 if templ_7745c5c3_Err != nil {
273256 return templ_7745c5c3_Err
274257 }
275- var templ_7745c5c3_Var12 string
276- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.PendingCount))
258+ var templ_7745c5c3_Var11 string
259+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.PendingCount))
277260 if templ_7745c5c3_Err != nil {
278- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 46, Col: 116}
261+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 45, Col: 116}
279262 }
280- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
263+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
281264 if templ_7745c5c3_Err != nil {
282265 return templ_7745c5c3_Err
283266 }
284- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></h2>")
267+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></h2>")
285268 if templ_7745c5c3_Err != nil {
286269 return templ_7745c5c3_Err
287270 }
288271 if len(p.Pending) > 0 {
289- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<ul class=\"pending-list\">")
272+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<ul class=\"pending-list\">")
290273 if templ_7745c5c3_Err != nil {
291274 return templ_7745c5c3_Err
292275 }
293276 for _, m := range p.Pending {
294- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<li class=\"pending\"><div class=\"p-head\"><a href=\"")
277+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<li class=\"pending\"><div class=\"p-head\"><a href=\"")
295278 if templ_7745c5c3_Err != nil {
296279 return templ_7745c5c3_Err
297280 }
298- var templ_7745c5c3_Var13 templ.SafeURL
299- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))
281+ var templ_7745c5c3_Var12 templ.SafeURL
282+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))
300283 if templ_7745c5c3_Err != nil {
301- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 52, Col: 42}
284+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 51, Col: 42}
302285 }
303- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
286+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
304287 if templ_7745c5c3_Err != nil {
305288 return templ_7745c5c3_Err
306289 }
307- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
290+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
308291 if templ_7745c5c3_Err != nil {
309292 return templ_7745c5c3_Err
310293 }
311- var templ_7745c5c3_Var14 string
312- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)
294+ var templ_7745c5c3_Var13 string
295+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)
313296 if templ_7745c5c3_Err != nil {
314- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 52, Col: 53}
297+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 51, Col: 53}
315298 }
316- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
299+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
317300 if templ_7745c5c3_Err != nil {
318301 return templ_7745c5c3_Err
319302 }
320- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a> <span>")
303+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a> <span>")
321304 if templ_7745c5c3_Err != nil {
322305 return templ_7745c5c3_Err
323306 }
324- var templ_7745c5c3_Var15 string
325- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author)
307+ var templ_7745c5c3_Var14 string
308+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author)
326309 if templ_7745c5c3_Err != nil {
327- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 53, Col: 23}
310+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 52, Col: 23}
328311 }
329- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
312+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
330313 if templ_7745c5c3_Err != nil {
331314 return templ_7745c5c3_Err
332315 }
333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span> ")
316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span> ")
334317 if templ_7745c5c3_Err != nil {
335318 return templ_7745c5c3_Err
336319 }
337320 if m.WhenAt != "" {
338- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<time datetime=\"")
321+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<time datetime=\"")
339322 if templ_7745c5c3_Err != nil {
340323 return templ_7745c5c3_Err
341324 }
342- var templ_7745c5c3_Var16 string
343- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
325+ var templ_7745c5c3_Var15 string
326+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
344327 if templ_7745c5c3_Err != nil {
345- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 55, Col: 33}
328+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 54, Col: 33}
346329 }
347- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
330+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
348331 if templ_7745c5c3_Err != nil {
349332 return templ_7745c5c3_Err
350333 }
351- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" title=\"")
334+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" title=\"")
352335 if templ_7745c5c3_Err != nil {
353336 return templ_7745c5c3_Err
354337 }
355- var templ_7745c5c3_Var17 string
356- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
338+ var templ_7745c5c3_Var16 string
339+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
357340 if templ_7745c5c3_Err != nil {
358- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 55, Col: 52}
341+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 54, Col: 52}
359342 }
360- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
343+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
361344 if templ_7745c5c3_Err != nil {
362345 return templ_7745c5c3_Err
363346 }
364- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
347+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
365348 if templ_7745c5c3_Err != nil {
366349 return templ_7745c5c3_Err
367350 }
368- var templ_7745c5c3_Var18 string
369- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
351+ var templ_7745c5c3_Var17 string
352+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
370353 if templ_7745c5c3_Err != nil {
371- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 55, Col: 63}
354+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 54, Col: 63}
372355 }
373- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
356+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
374357 if templ_7745c5c3_Err != nil {
375358 return templ_7745c5c3_Err
376359 }
377- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</time>")
360+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</time>")
378361 if templ_7745c5c3_Err != nil {
379362 return templ_7745c5c3_Err
380363 }
381364 }
382- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div><p class=\"p-title\">")
365+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><p class=\"p-title\">")
383366 if templ_7745c5c3_Err != nil {
384367 return templ_7745c5c3_Err
385368 }
386- var templ_7745c5c3_Var19 string
387- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
369+ var templ_7745c5c3_Var18 string
370+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
388371 if templ_7745c5c3_Err != nil {
389- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 58, Col: 34}
372+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 57, Col: 34}
390373 }
391- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
374+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
392375 if templ_7745c5c3_Err != nil {
393376 return templ_7745c5c3_Err
394377 }
395- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p>")
378+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p>")
396379 if templ_7745c5c3_Err != nil {
397380 return templ_7745c5c3_Err
398381 }
399382 if m.Preview != "" {
400- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p class=\"p-body\">")
383+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"p-body\">")
401384 if templ_7745c5c3_Err != nil {
402385 return templ_7745c5c3_Err
403386 }
404- var templ_7745c5c3_Var20 string
405- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(m.Preview)
387+ var templ_7745c5c3_Var19 string
388+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.Preview)
406389 if templ_7745c5c3_Err != nil {
407- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 60, Col: 36}
390+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 59, Col: 36}
408391 }
409- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
392+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
410393 if templ_7745c5c3_Err != nil {
411394 return templ_7745c5c3_Err
412395 }
413- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</p>")
396+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p>")
414397 if templ_7745c5c3_Err != nil {
415398 return templ_7745c5c3_Err
416399 }
417400 }
418401 if m.Approve != "" {
419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"p-btns\"><form method=\"post\" action=\"")
402+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"p-btns\"><form method=\"post\" action=\"")
420403 if templ_7745c5c3_Err != nil {
421404 return templ_7745c5c3_Err
422405 }
423- var templ_7745c5c3_Var21 templ.SafeURL
424- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Approve))
406+ var templ_7745c5c3_Var20 templ.SafeURL
407+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Approve))
425408 if templ_7745c5c3_Err != nil {
426- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 64, Col: 61}
409+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 63, Col: 61}
427410 }
428- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
411+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
429412 if templ_7745c5c3_Err != nil {
430413 return templ_7745c5c3_Err
431414 }
432- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><button type=\"submit\" class=\"btn primary\">Approve</button></form><form method=\"post\" action=\"")
415+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\"><button type=\"submit\" class=\"btn primary\">Approve</button></form><form method=\"post\" action=\"")
433416 if templ_7745c5c3_Err != nil {
434417 return templ_7745c5c3_Err
435418 }
436- var templ_7745c5c3_Var22 templ.SafeURL
437- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Reject))
419+ var templ_7745c5c3_Var21 templ.SafeURL
420+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Reject))
438421 if templ_7745c5c3_Err != nil {
439- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 65, Col: 60}
422+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 64, Col: 60}
440423 }
441- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
424+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
442425 if templ_7745c5c3_Err != nil {
443426 return templ_7745c5c3_Err
444427 }
445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"><button type=\"submit\" class=\"btn\">Reject</button></form><form method=\"post\" action=\"")
428+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><button type=\"submit\" class=\"btn\">Reject</button></form><form method=\"post\" action=\"")
446429 if templ_7745c5c3_Err != nil {
447430 return templ_7745c5c3_Err
448431 }
449- var templ_7745c5c3_Var23 templ.SafeURL
450- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Ban))
432+ var templ_7745c5c3_Var22 templ.SafeURL
433+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Ban))
451434 if templ_7745c5c3_Err != nil {
452- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 66, Col: 57}
435+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 65, Col: 57}
453436 }
454- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
437+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
455438 if templ_7745c5c3_Err != nil {
456439 return templ_7745c5c3_Err
457440 }
458- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"><button type=\"submit\" class=\"btn danger\">Reject and ban</button></form></div>")
441+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"><button type=\"submit\" class=\"btn danger\">Reject and ban</button></form></div>")
459442 if templ_7745c5c3_Err != nil {
460443 return templ_7745c5c3_Err
461444 }
462445 }
463- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</li>")
446+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</li>")
464447 if templ_7745c5c3_Err != nil {
465448 return templ_7745c5c3_Err
466449 }
467450 }
468- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</ul>")
451+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</ul>")
469452 if templ_7745c5c3_Err != nil {
470453 return templ_7745c5c3_Err
471454 }
472455 } else {
473- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<p class=\"lede dim\">nothing awaits moderation.</p>")
456+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<p class=\"lede dim\">nothing awaits moderation.</p>")
474457 if templ_7745c5c3_Err != nil {
475458 return templ_7745c5c3_Err
476459 }
477460 }
478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</section>")
461+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</section>")
479462 if templ_7745c5c3_Err != nil {
480463 return templ_7745c5c3_Err
481464 }
@@ -499,25 +482,25 @@ func InboxMRs(p InboxPage) templ.Component {
499482 }()
500483 }
501484 ctx = templ.InitializeContext(ctx)
502- templ_7745c5c3_Var24 := templ.GetChildren(ctx)
503- if templ_7745c5c3_Var24 == nil {
504- templ_7745c5c3_Var24 = templ.NopComponent
485+ templ_7745c5c3_Var23 := templ.GetChildren(ctx)
486+ if templ_7745c5c3_Var23 == nil {
487+ templ_7745c5c3_Var23 = templ.NopComponent
505488 }
506489 ctx = templ.ClearChildren(ctx)
507- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<section id=\"inbox-mrs\" aria-labelledby=\"inbox-mrs-h\"><h2 id=\"inbox-mrs-h\" class=\"rule-h\"><span>Open merge requests</span><span class=\"count\">")
490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<section id=\"inbox-mrs\" aria-labelledby=\"inbox-mrs-h\"><h2 id=\"inbox-mrs-h\" class=\"rule-h\"><span>Open merge requests</span><span class=\"count\">")
508491 if templ_7745c5c3_Err != nil {
509492 return templ_7745c5c3_Err
510493 }
511- var templ_7745c5c3_Var25 string
512- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.MRs)))
494+ var templ_7745c5c3_Var24 string
495+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.MRs)))
513496 if templ_7745c5c3_Err != nil {
514- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 80, Col: 108}
497+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 79, Col: 108}
515498 }
516- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
499+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
517500 if templ_7745c5c3_Err != nil {
518501 return templ_7745c5c3_Err
519502 }
520- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span></h2>")
503+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span></h2>")
521504 if templ_7745c5c3_Err != nil {
522505 return templ_7745c5c3_Err
523506 }
@@ -526,12 +509,12 @@ func InboxMRs(p InboxPage) templ.Component {
526509 return templ_7745c5c3_Err
527510 }
528511 if len(p.MRs) == 0 {
529- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<p class=\"lede dim\">no open merge requests.</p>")
512+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<p class=\"lede dim\">no open merge requests.</p>")
530513 if templ_7745c5c3_Err != nil {
531514 return templ_7745c5c3_Err
532515 }
533516 }
534- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</section>")
517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</section>")
535518 if templ_7745c5c3_Err != nil {
536519 return templ_7745c5c3_Err
537520 }
@@ -555,25 +538,25 @@ func InboxIssues(p InboxPage) templ.Component {
555538 }()
556539 }
557540 ctx = templ.InitializeContext(ctx)
558- templ_7745c5c3_Var26 := templ.GetChildren(ctx)
559- if templ_7745c5c3_Var26 == nil {
560- templ_7745c5c3_Var26 = templ.NopComponent
541+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
542+ if templ_7745c5c3_Var25 == nil {
543+ templ_7745c5c3_Var25 = templ.NopComponent
561544 }
562545 ctx = templ.ClearChildren(ctx)
563- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<section id=\"inbox-issues\" aria-labelledby=\"inbox-issues-h\"><h2 id=\"inbox-issues-h\" class=\"rule-h\"><span>Open issues</span><span class=\"count\">")
546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<section id=\"inbox-issues\" aria-labelledby=\"inbox-issues-h\"><h2 id=\"inbox-issues-h\" class=\"rule-h\"><span>Open issues</span><span class=\"count\">")
564547 if templ_7745c5c3_Err != nil {
565548 return templ_7745c5c3_Err
566549 }
567- var templ_7745c5c3_Var27 string
568- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.IssueRows)))
550+ var templ_7745c5c3_Var26 string
551+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.IssueRows)))
569552 if templ_7745c5c3_Err != nil {
570- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 90, Col: 109}
553+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 89, Col: 109}
571554 }
572- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
555+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
573556 if templ_7745c5c3_Err != nil {
574557 return templ_7745c5c3_Err
575558 }
576- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</span></h2>")
559+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span></h2>")
577560 if templ_7745c5c3_Err != nil {
578561 return templ_7745c5c3_Err
579562 }
@@ -582,12 +565,12 @@ func InboxIssues(p InboxPage) templ.Component {
582565 return templ_7745c5c3_Err
583566 }
584567 if len(p.IssueRows) == 0 {
585- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<p class=\"lede dim\">no open issues.</p>")
568+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<p class=\"lede dim\">no open issues.</p>")
586569 if templ_7745c5c3_Err != nil {
587570 return templ_7745c5c3_Err
588571 }
589572 }
590- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</section>")
573+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</section>")
591574 if templ_7745c5c3_Err != nil {
592575 return templ_7745c5c3_Err
593576 }
@@ -611,160 +594,160 @@ func inboxRows(rows []InboxRow, label string) templ.Component {
611594 }()
612595 }
613596 ctx = templ.InitializeContext(ctx)
614- templ_7745c5c3_Var28 := templ.GetChildren(ctx)
615- if templ_7745c5c3_Var28 == nil {
616- templ_7745c5c3_Var28 = templ.NopComponent
597+ templ_7745c5c3_Var27 := templ.GetChildren(ctx)
598+ if templ_7745c5c3_Var27 == nil {
599+ templ_7745c5c3_Var27 = templ.NopComponent
617600 }
618601 ctx = templ.ClearChildren(ctx)
619602 if len(rows) > 0 {
620- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<table class=\"list mrs\" aria-label=\"")
603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<table class=\"list mrs\" aria-label=\"")
621604 if templ_7745c5c3_Err != nil {
622605 return templ_7745c5c3_Err
623606 }
624- var templ_7745c5c3_Var29 string
625- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(label)
607+ var templ_7745c5c3_Var28 string
608+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(label)
626609 if templ_7745c5c3_Err != nil {
627- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 100, Col: 44}
610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 99, Col: 44}
628611 }
629- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
630613 if templ_7745c5c3_Err != nil {
631614 return templ_7745c5c3_Err
632615 }
633- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"><tbody data-rows>")
616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"><tbody data-rows>")
634617 if templ_7745c5c3_Err != nil {
635618 return templ_7745c5c3_Err
636619 }
637620 for _, m := range rows {
638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<tr")
621+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<tr")
639622 if templ_7745c5c3_Err != nil {
640623 return templ_7745c5c3_Err
641624 }
642625 if m.Cursor {
643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " data-cursor")
626+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " data-cursor")
644627 if templ_7745c5c3_Err != nil {
645628 return templ_7745c5c3_Err
646629 }
647630 }
648- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " data-state=\"open\"><td class=\"c-n mute\">")
631+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " data-state=\"open\"><td class=\"c-n mute\">")
649632 if templ_7745c5c3_Err != nil {
650633 return templ_7745c5c3_Err
651634 }
652- var templ_7745c5c3_Var30 string
653- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number)
635+ var templ_7745c5c3_Var29 string
636+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number)
654637 if templ_7745c5c3_Err != nil {
655- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 37}
638+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 103, Col: 37}
656639 }
657- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
640+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
658641 if templ_7745c5c3_Err != nil {
659642 return templ_7745c5c3_Err
660643 }
661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</td><td class=\"c-subject\"><a href=\"")
644+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</td><td class=\"c-subject\"><a href=\"")
662645 if templ_7745c5c3_Err != nil {
663646 return templ_7745c5c3_Err
664647 }
665- var templ_7745c5c3_Var31 templ.SafeURL
666- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href))
648+ var templ_7745c5c3_Var30 templ.SafeURL
649+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href))
667650 if templ_7745c5c3_Err != nil {
668- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 59}
651+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 59}
669652 }
670- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
653+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
671654 if templ_7745c5c3_Err != nil {
672655 return templ_7745c5c3_Err
673656 }
674- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">")
657+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\">")
675658 if templ_7745c5c3_Err != nil {
676659 return templ_7745c5c3_Err
677660 }
678- var templ_7745c5c3_Var32 string
679- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
661+ var templ_7745c5c3_Var31 string
662+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
680663 if templ_7745c5c3_Err != nil {
681- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 71}
664+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 71}
682665 }
683- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
666+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
684667 if templ_7745c5c3_Err != nil {
685668 return templ_7745c5c3_Err
686669 }
687- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</a> <a class=\"mute\" href=\"")
670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</a> <a class=\"mute\" href=\"")
688671 if templ_7745c5c3_Err != nil {
689672 return templ_7745c5c3_Err
690673 }
691- var templ_7745c5c3_Var33 templ.SafeURL
692- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))
674+ var templ_7745c5c3_Var32 templ.SafeURL
675+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))
693676 if templ_7745c5c3_Err != nil {
694- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 126}
677+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 126}
695678 }
696- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
679+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
697680 if templ_7745c5c3_Err != nil {
698681 return templ_7745c5c3_Err
699682 }
700- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">")
683+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">")
701684 if templ_7745c5c3_Err != nil {
702685 return templ_7745c5c3_Err
703686 }
704- var templ_7745c5c3_Var34 string
705- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)
687+ var templ_7745c5c3_Var33 string
688+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)
706689 if templ_7745c5c3_Err != nil {
707- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 137}
690+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 137}
708691 }
709- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
692+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
710693 if templ_7745c5c3_Err != nil {
711694 return templ_7745c5c3_Err
712695 }
713- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</a></td><td class=\"c-when nowrap dim\">")
696+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</a></td><td class=\"c-when nowrap dim\">")
714697 if templ_7745c5c3_Err != nil {
715698 return templ_7745c5c3_Err
716699 }
717700 if m.WhenAt != "" {
718- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<time datetime=\"")
701+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<time datetime=\"")
719702 if templ_7745c5c3_Err != nil {
720703 return templ_7745c5c3_Err
721704 }
722- var templ_7745c5c3_Var35 string
723- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
705+ var templ_7745c5c3_Var34 string
706+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
724707 if templ_7745c5c3_Err != nil {
725- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 108, Col: 33}
708+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 107, Col: 33}
726709 }
727- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
710+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
728711 if templ_7745c5c3_Err != nil {
729712 return templ_7745c5c3_Err
730713 }
731- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" title=\"")
714+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" title=\"")
732715 if templ_7745c5c3_Err != nil {
733716 return templ_7745c5c3_Err
734717 }
735- var templ_7745c5c3_Var36 string
736- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
718+ var templ_7745c5c3_Var35 string
719+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
737720 if templ_7745c5c3_Err != nil {
738- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 108, Col: 52}
721+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 107, Col: 52}
739722 }
740- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
723+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
741724 if templ_7745c5c3_Err != nil {
742725 return templ_7745c5c3_Err
743726 }
744- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\">")
727+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\">")
745728 if templ_7745c5c3_Err != nil {
746729 return templ_7745c5c3_Err
747730 }
748- var templ_7745c5c3_Var37 string
749- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
731+ var templ_7745c5c3_Var36 string
732+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
750733 if templ_7745c5c3_Err != nil {
751- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 108, Col: 63}
734+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 107, Col: 63}
752735 }
753- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
736+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
754737 if templ_7745c5c3_Err != nil {
755738 return templ_7745c5c3_Err
756739 }
757- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</time>")
740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</time>")
758741 if templ_7745c5c3_Err != nil {
759742 return templ_7745c5c3_Err
760743 }
761744 }
762- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</td></tr>")
745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</td></tr>")
763746 if templ_7745c5c3_Err != nil {
764747 return templ_7745c5c3_Err
765748 }
766749 }
767- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</tbody></table>")
750+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</tbody></table>")
768751 if templ_7745c5c3_Err != nil {
769752 return templ_7745c5c3_Err
770753 }
@@ -773,265 +756,4 @@ func inboxRows(rows []InboxRow, label string) templ.Component {
773756 })
774757 }
775758
776-// InboxRuns stays as an empty fragment when CI is disabled.
777-func InboxRuns(p InboxPage) templ.Component {
778- return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
779- templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
780- if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
781- return templ_7745c5c3_CtxErr
782- }
783- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
784- if !templ_7745c5c3_IsBuffer {
785- defer func() {
786- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
787- if templ_7745c5c3_Err == nil {
788- templ_7745c5c3_Err = templ_7745c5c3_BufErr
789- }
790- }()
791- }
792- ctx = templ.InitializeContext(ctx)
793- templ_7745c5c3_Var38 := templ.GetChildren(ctx)
794- if templ_7745c5c3_Var38 == nil {
795- templ_7745c5c3_Var38 = templ.NopComponent
796- }
797- ctx = templ.ClearChildren(ctx)
798- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<section id=\"inbox-runs\"")
799- if templ_7745c5c3_Err != nil {
800- return templ_7745c5c3_Err
801- }
802- if !p.RunsEnabled {
803- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " hidden")
804- if templ_7745c5c3_Err != nil {
805- return templ_7745c5c3_Err
806- }
807- }
808- if p.RunsEnabled {
809- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " aria-labelledby=\"inbox-runs-h\"")
810- if templ_7745c5c3_Err != nil {
811- return templ_7745c5c3_Err
812- }
813- }
814- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, ">")
815- if templ_7745c5c3_Err != nil {
816- return templ_7745c5c3_Err
817- }
818- if p.RunsEnabled {
819- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<h2 id=\"inbox-runs-h\" class=\"rule-h\"><span>Running</span><span class=\"count\">")
820- if templ_7745c5c3_Err != nil {
821- return templ_7745c5c3_Err
822- }
823- var templ_7745c5c3_Var39 string
824- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Runs)))
825- if templ_7745c5c3_Err != nil {
826- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 126, Col: 99}
827- }
828- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
829- if templ_7745c5c3_Err != nil {
830- return templ_7745c5c3_Err
831- }
832- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span></h2>")
833- if templ_7745c5c3_Err != nil {
834- return templ_7745c5c3_Err
835- }
836- if len(p.Runs) == 0 {
837- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<p class=\"lede dim\">no runs in progress.</p>")
838- if templ_7745c5c3_Err != nil {
839- return templ_7745c5c3_Err
840- }
841- } else {
842- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<table class=\"list mrs\" aria-label=\"Running\"><tbody data-rows>")
843- if templ_7745c5c3_Err != nil {
844- return templ_7745c5c3_Err
845- }
846- for _, r := range p.Runs {
847- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<tr><td class=\"c-st\">")
848- if templ_7745c5c3_Err != nil {
849- return templ_7745c5c3_Err
850- }
851- templ_7745c5c3_Err = CheckIcon(r.Kind, r.State).Render(ctx, templ_7745c5c3_Buffer)
852- if templ_7745c5c3_Err != nil {
853- return templ_7745c5c3_Err
854- }
855- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<span class=\"dim\">")
856- if templ_7745c5c3_Err != nil {
857- return templ_7745c5c3_Err
858- }
859- var templ_7745c5c3_Var40 string
860- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
861- if templ_7745c5c3_Err != nil {
862- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 134, Col: 79}
863- }
864- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
865- if templ_7745c5c3_Err != nil {
866- return templ_7745c5c3_Err
867- }
868- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</span></td><td class=\"c-subject\"><a href=\"")
869- if templ_7745c5c3_Err != nil {
870- return templ_7745c5c3_Err
871- }
872- var templ_7745c5c3_Var41 templ.SafeURL
873- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
874- if templ_7745c5c3_Err != nil {
875- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 61}
876- }
877- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
878- if templ_7745c5c3_Err != nil {
879- return templ_7745c5c3_Err
880- }
881- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\">")
882- if templ_7745c5c3_Err != nil {
883- return templ_7745c5c3_Err
884- }
885- var templ_7745c5c3_Var42 string
886- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(r.Workflow)
887- if templ_7745c5c3_Err != nil {
888- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 76}
889- }
890- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
891- if templ_7745c5c3_Err != nil {
892- return templ_7745c5c3_Err
893- }
894- var templ_7745c5c3_Var43 string
895- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
896- if templ_7745c5c3_Err != nil {
897- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 83}
898- }
899- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
900- if templ_7745c5c3_Err != nil {
901- return templ_7745c5c3_Err
902- }
903- var templ_7745c5c3_Var44 string
904- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
905- if templ_7745c5c3_Err != nil {
906- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 95}
907- }
908- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
909- if templ_7745c5c3_Err != nil {
910- return templ_7745c5c3_Err
911- }
912- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</a> <a class=\"mute\" href=\"")
913- if templ_7745c5c3_Err != nil {
914- return templ_7745c5c3_Err
915- }
916- var templ_7745c5c3_Var45 templ.SafeURL
917- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.RepoHref))
918- if templ_7745c5c3_Err != nil {
919- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 150}
920- }
921- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
922- if templ_7745c5c3_Err != nil {
923- return templ_7745c5c3_Err
924- }
925- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">")
926- if templ_7745c5c3_Err != nil {
927- return templ_7745c5c3_Err
928- }
929- var templ_7745c5c3_Var46 string
930- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Repo)
931- if templ_7745c5c3_Err != nil {
932- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 161}
933- }
934- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
935- if templ_7745c5c3_Err != nil {
936- return templ_7745c5c3_Err
937- }
938- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</a> <span class=\"dim\">")
939- if templ_7745c5c3_Err != nil {
940- return templ_7745c5c3_Err
941- }
942- var templ_7745c5c3_Var47 string
943- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(r.Ref)
944- if templ_7745c5c3_Err != nil {
945- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 193}
946- }
947- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
948- if templ_7745c5c3_Err != nil {
949- return templ_7745c5c3_Err
950- }
951- var templ_7745c5c3_Var48 string
952- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
953- if templ_7745c5c3_Err != nil {
954- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 200}
955- }
956- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
957- if templ_7745c5c3_Err != nil {
958- return templ_7745c5c3_Err
959- }
960- var templ_7745c5c3_Var49 string
961- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(r.Event)
962- if templ_7745c5c3_Err != nil {
963- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 211}
964- }
965- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
966- if templ_7745c5c3_Err != nil {
967- return templ_7745c5c3_Err
968- }
969- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span></td><td class=\"c-when nowrap dim\">")
970- if templ_7745c5c3_Err != nil {
971- return templ_7745c5c3_Err
972- }
973- if r.WhenAt != "" {
974- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<time datetime=\"")
975- if templ_7745c5c3_Err != nil {
976- return templ_7745c5c3_Err
977- }
978- var templ_7745c5c3_Var50 string
979- templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
980- if templ_7745c5c3_Err != nil {
981- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 138, Col: 35}
982- }
983- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
984- if templ_7745c5c3_Err != nil {
985- return templ_7745c5c3_Err
986- }
987- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" title=\"")
988- if templ_7745c5c3_Err != nil {
989- return templ_7745c5c3_Err
990- }
991- var templ_7745c5c3_Var51 string
992- templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
993- if templ_7745c5c3_Err != nil {
994- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 138, Col: 54}
995- }
996- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
997- if templ_7745c5c3_Err != nil {
998- return templ_7745c5c3_Err
999- }
1000- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">")
1001- if templ_7745c5c3_Err != nil {
1002- return templ_7745c5c3_Err
1003- }
1004- var templ_7745c5c3_Var52 string
1005- templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
1006- if templ_7745c5c3_Err != nil {
1007- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 138, Col: 65}
1008- }
1009- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
1010- if templ_7745c5c3_Err != nil {
1011- return templ_7745c5c3_Err
1012- }
1013- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</time>")
1014- if templ_7745c5c3_Err != nil {
1015- return templ_7745c5c3_Err
1016- }
1017- }
1018- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</td></tr>")
1019- if templ_7745c5c3_Err != nil {
1020- return templ_7745c5c3_Err
1021- }
1022- }
1023- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</tbody></table>")
1024- if templ_7745c5c3_Err != nil {
1025- return templ_7745c5c3_Err
1026- }
1027- }
1028- }
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</section>")
1030- if templ_7745c5c3_Err != nil {
1031- return templ_7745c5c3_Err
1032- }
1033- return nil
1034- })
1035-}
1036-
1037759 var _ = templruntime.GeneratedTemplate
@@ -62,10 +62,6 @@ func Inbox(l Layout, p InboxPage) templ.Component {
62 if templ_7745c5c3_Err != nil {62 if templ_7745c5c3_Err != nil {
63 return templ_7745c5c3_Err63 return templ_7745c5c3_Err
64 }64 }
65- templ_7745c5c3_Err = InboxRuns(p).Render(ctx, templ_7745c5c3_Buffer)
66- if templ_7745c5c3_Err != nil {
67- return templ_7745c5c3_Err
68- }
69 templ_7745c5c3_Err = InboxMRs(p).Render(ctx, templ_7745c5c3_Buffer)65 templ_7745c5c3_Err = InboxMRs(p).Render(ctx, templ_7745c5c3_Buffer)
70 if templ_7745c5c3_Err != nil {66 if templ_7745c5c3_Err != nil {
71 return templ_7745c5c3_Err67 return templ_7745c5c3_Err
@@ -116,7 +112,7 @@ func InboxCounters(p InboxPage) templ.Component {
116 var templ_7745c5c3_Var4 string112 var templ_7745c5c3_Var4 string
117 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.MergeRequests))113 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.MergeRequests))
118 if templ_7745c5c3_Err != nil {114 if templ_7745c5c3_Err != nil {
119- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 33}115+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 23, Col: 33}
120 }116 }
121 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))117 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
122 if templ_7745c5c3_Err != nil {118 if templ_7745c5c3_Err != nil {
@@ -129,7 +125,7 @@ func InboxCounters(p InboxPage) templ.Component {
129 var templ_7745c5c3_Var5 string125 var templ_7745c5c3_Var5 string
130 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Issues))126 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Issues))
131 if templ_7745c5c3_Err != nil {127 if templ_7745c5c3_Err != nil {
132- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 87}128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 23, Col: 87}
133 }129 }
134 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))130 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
135 if templ_7745c5c3_Err != nil {131 if templ_7745c5c3_Err != nil {
@@ -142,26 +138,13 @@ func InboxCounters(p InboxPage) templ.Component {
142 var templ_7745c5c3_Var6 string138 var templ_7745c5c3_Var6 string
143 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.PendingCount))139 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.PendingCount))
144 if templ_7745c5c3_Err != nil {140 if templ_7745c5c3_Err != nil {
145- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 139}141+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 23, Col: 139}
146 }142 }
147 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))143 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
148 if templ_7745c5c3_Err != nil {144 if templ_7745c5c3_Err != nil {
149 return templ_7745c5c3_Err145 return templ_7745c5c3_Err
150 }146 }
151- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</strong> awaiting moderation · <strong>")147+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</strong> awaiting moderation</p>")
152- if templ_7745c5c3_Err != nil {
153- return templ_7745c5c3_Err
154- }
155- var templ_7745c5c3_Var7 string
156- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Running))
157- if templ_7745c5c3_Err != nil {
158- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 24, Col: 199}
159- }
160- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
161- if templ_7745c5c3_Err != nil {
162- return templ_7745c5c3_Err
163- }
164- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</strong> running</p>")
165 if templ_7745c5c3_Err != nil {148 if templ_7745c5c3_Err != nil {
166 return templ_7745c5c3_Err149 return templ_7745c5c3_Err
167 }150 }
@@ -185,58 +168,58 @@ func InboxRepos(p InboxPage) templ.Component {
185 }()168 }()
186 }169 }
187 ctx = templ.InitializeContext(ctx)170 ctx = templ.InitializeContext(ctx)
188- templ_7745c5c3_Var8 := templ.GetChildren(ctx)171+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
189- if templ_7745c5c3_Var8 == nil {172+ if templ_7745c5c3_Var7 == nil {
190- templ_7745c5c3_Var8 = templ.NopComponent173+ templ_7745c5c3_Var7 = templ.NopComponent
191 }174 }
192 ctx = templ.ClearChildren(ctx)175 ctx = templ.ClearChildren(ctx)
193- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<nav id=\"inbox-repos\" class=\"since\" aria-label=\"Repository\">")176+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<nav id=\"inbox-repos\" class=\"since\" aria-label=\"Repository\">")
194 if templ_7745c5c3_Err != nil {177 if templ_7745c5c3_Err != nil {
195 return templ_7745c5c3_Err178 return templ_7745c5c3_Err
196 }179 }
197 for _, r := range p.Repos {180 for _, r := range p.Repos {
198- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
199 if templ_7745c5c3_Err != nil {182 if templ_7745c5c3_Err != nil {
200 return templ_7745c5c3_Err183 return templ_7745c5c3_Err
201 }184 }
202- var templ_7745c5c3_Var9 templ.SafeURL185+ var templ_7745c5c3_Var8 templ.SafeURL
203- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))186+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
204 if templ_7745c5c3_Err != nil {187 if templ_7745c5c3_Err != nil {
205- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 32, Col: 32}188+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 31, Col: 32}
206 }189 }
207- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))190+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
208 if templ_7745c5c3_Err != nil {191 if templ_7745c5c3_Err != nil {
209 return templ_7745c5c3_Err192 return templ_7745c5c3_Err
210 }193 }
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"")194+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"")
212 if templ_7745c5c3_Err != nil {195 if templ_7745c5c3_Err != nil {
213 return templ_7745c5c3_Err196 return templ_7745c5c3_Err
214 }197 }
215 if r.Current {198 if r.Current {
216- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " aria-current=\"page\"")199+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " aria-current=\"page\"")
217 if templ_7745c5c3_Err != nil {200 if templ_7745c5c3_Err != nil {
218 return templ_7745c5c3_Err201 return templ_7745c5c3_Err
219 }202 }
220 }203 }
221- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ">")204+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">")
222 if templ_7745c5c3_Err != nil {205 if templ_7745c5c3_Err != nil {
223 return templ_7745c5c3_Err206 return templ_7745c5c3_Err
224 }207 }
225- var templ_7745c5c3_Var10 string208+ var templ_7745c5c3_Var9 string
226- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(r.Label)209+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(r.Label)
227 if templ_7745c5c3_Err != nil {210 if templ_7745c5c3_Err != nil {
228- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 36, Col: 13}211+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 35, Col: 13}
229 }212 }
230- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))213+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
231 if templ_7745c5c3_Err != nil {214 if templ_7745c5c3_Err != nil {
232 return templ_7745c5c3_Err215 return templ_7745c5c3_Err
233 }216 }
234- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</a>")217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</a>")
235 if templ_7745c5c3_Err != nil {218 if templ_7745c5c3_Err != nil {
236 return templ_7745c5c3_Err219 return templ_7745c5c3_Err
237 }220 }
238 }221 }
239- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</nav>")222+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</nav>")
240 if templ_7745c5c3_Err != nil {223 if templ_7745c5c3_Err != nil {
241 return templ_7745c5c3_Err224 return templ_7745c5c3_Err
242 }225 }
@@ -263,219 +246,219 @@ func InboxPending(p InboxPage) templ.Component {
263 }()246 }()
264 }247 }
265 ctx = templ.InitializeContext(ctx)248 ctx = templ.InitializeContext(ctx)
266- templ_7745c5c3_Var11 := templ.GetChildren(ctx)249+ templ_7745c5c3_Var10 := templ.GetChildren(ctx)
267- if templ_7745c5c3_Var11 == nil {250+ if templ_7745c5c3_Var10 == nil {
268- templ_7745c5c3_Var11 = templ.NopComponent251+ templ_7745c5c3_Var10 = templ.NopComponent
269 }252 }
270 ctx = templ.ClearChildren(ctx)253 ctx = templ.ClearChildren(ctx)
271- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<section id=\"inbox-pending\" aria-labelledby=\"inbox-pending-h\"><h2 id=\"inbox-pending-h\" class=\"rule-h\"><span>Awaiting moderation</span><span class=\"count\">")254+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<section id=\"inbox-pending\" aria-labelledby=\"inbox-pending-h\"><h2 id=\"inbox-pending-h\" class=\"rule-h\"><span>Awaiting moderation</span><span class=\"count\">")
272 if templ_7745c5c3_Err != nil {255 if templ_7745c5c3_Err != nil {
273 return templ_7745c5c3_Err256 return templ_7745c5c3_Err
274 }257 }
275- var templ_7745c5c3_Var12 string258+ var templ_7745c5c3_Var11 string
276- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.PendingCount))259+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.PendingCount))
277 if templ_7745c5c3_Err != nil {260 if templ_7745c5c3_Err != nil {
278- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 46, Col: 116}261+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 45, Col: 116}
279 }262 }
280- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))263+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
281 if templ_7745c5c3_Err != nil {264 if templ_7745c5c3_Err != nil {
282 return templ_7745c5c3_Err265 return templ_7745c5c3_Err
283 }266 }
284- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></h2>")267+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></h2>")
285 if templ_7745c5c3_Err != nil {268 if templ_7745c5c3_Err != nil {
286 return templ_7745c5c3_Err269 return templ_7745c5c3_Err
287 }270 }
288 if len(p.Pending) > 0 {271 if len(p.Pending) > 0 {
289- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<ul class=\"pending-list\">")272+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<ul class=\"pending-list\">")
290 if templ_7745c5c3_Err != nil {273 if templ_7745c5c3_Err != nil {
291 return templ_7745c5c3_Err274 return templ_7745c5c3_Err
292 }275 }
293 for _, m := range p.Pending {276 for _, m := range p.Pending {
294- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<li class=\"pending\"><div class=\"p-head\"><a href=\"")277+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<li class=\"pending\"><div class=\"p-head\"><a href=\"")
295 if templ_7745c5c3_Err != nil {278 if templ_7745c5c3_Err != nil {
296 return templ_7745c5c3_Err279 return templ_7745c5c3_Err
297 }280 }
298- var templ_7745c5c3_Var13 templ.SafeURL281+ var templ_7745c5c3_Var12 templ.SafeURL
299- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))282+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))
300 if templ_7745c5c3_Err != nil {283 if templ_7745c5c3_Err != nil {
301- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 52, Col: 42}284+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 51, Col: 42}
302 }285 }
303- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))286+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
304 if templ_7745c5c3_Err != nil {287 if templ_7745c5c3_Err != nil {
305 return templ_7745c5c3_Err288 return templ_7745c5c3_Err
306 }289 }
307- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")290+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
308 if templ_7745c5c3_Err != nil {291 if templ_7745c5c3_Err != nil {
309 return templ_7745c5c3_Err292 return templ_7745c5c3_Err
310 }293 }
311- var templ_7745c5c3_Var14 string294+ var templ_7745c5c3_Var13 string
312- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)295+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)
313 if templ_7745c5c3_Err != nil {296 if templ_7745c5c3_Err != nil {
314- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 52, Col: 53}297+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 51, Col: 53}
315 }298 }
316- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))299+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
317 if templ_7745c5c3_Err != nil {300 if templ_7745c5c3_Err != nil {
318 return templ_7745c5c3_Err301 return templ_7745c5c3_Err
319 }302 }
320- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a> <span>")303+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a> <span>")
321 if templ_7745c5c3_Err != nil {304 if templ_7745c5c3_Err != nil {
322 return templ_7745c5c3_Err305 return templ_7745c5c3_Err
323 }306 }
324- var templ_7745c5c3_Var15 string307+ var templ_7745c5c3_Var14 string
325- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author)308+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author)
326 if templ_7745c5c3_Err != nil {309 if templ_7745c5c3_Err != nil {
327- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 53, Col: 23}310+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 52, Col: 23}
328 }311 }
329- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))312+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
330 if templ_7745c5c3_Err != nil {313 if templ_7745c5c3_Err != nil {
331 return templ_7745c5c3_Err314 return templ_7745c5c3_Err
332 }315 }
333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span> ")316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span> ")
334 if templ_7745c5c3_Err != nil {317 if templ_7745c5c3_Err != nil {
335 return templ_7745c5c3_Err318 return templ_7745c5c3_Err
336 }319 }
337 if m.WhenAt != "" {320 if m.WhenAt != "" {
338- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<time datetime=\"")321+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<time datetime=\"")
339 if templ_7745c5c3_Err != nil {322 if templ_7745c5c3_Err != nil {
340 return templ_7745c5c3_Err323 return templ_7745c5c3_Err
341 }324 }
342- var templ_7745c5c3_Var16 string325+ var templ_7745c5c3_Var15 string
343- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)326+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
344 if templ_7745c5c3_Err != nil {327 if templ_7745c5c3_Err != nil {
345- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 55, Col: 33}328+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 54, Col: 33}
346 }329 }
347- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)330+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
348 if templ_7745c5c3_Err != nil {331 if templ_7745c5c3_Err != nil {
349 return templ_7745c5c3_Err332 return templ_7745c5c3_Err
350 }333 }
351- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" title=\"")334+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" title=\"")
352 if templ_7745c5c3_Err != nil {335 if templ_7745c5c3_Err != nil {
353 return templ_7745c5c3_Err336 return templ_7745c5c3_Err
354 }337 }
355- var templ_7745c5c3_Var17 string338+ var templ_7745c5c3_Var16 string
356- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)339+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
357 if templ_7745c5c3_Err != nil {340 if templ_7745c5c3_Err != nil {
358- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 55, Col: 52}341+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 54, Col: 52}
359 }342 }
360- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)343+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
361 if templ_7745c5c3_Err != nil {344 if templ_7745c5c3_Err != nil {
362 return templ_7745c5c3_Err345 return templ_7745c5c3_Err
363 }346 }
364- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")347+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
365 if templ_7745c5c3_Err != nil {348 if templ_7745c5c3_Err != nil {
366 return templ_7745c5c3_Err349 return templ_7745c5c3_Err
367 }350 }
368- var templ_7745c5c3_Var18 string351+ var templ_7745c5c3_Var17 string
369- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)352+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
370 if templ_7745c5c3_Err != nil {353 if templ_7745c5c3_Err != nil {
371- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 55, Col: 63}354+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 54, Col: 63}
372 }355 }
373- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))356+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
374 if templ_7745c5c3_Err != nil {357 if templ_7745c5c3_Err != nil {
375 return templ_7745c5c3_Err358 return templ_7745c5c3_Err
376 }359 }
377- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</time>")360+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</time>")
378 if templ_7745c5c3_Err != nil {361 if templ_7745c5c3_Err != nil {
379 return templ_7745c5c3_Err362 return templ_7745c5c3_Err
380 }363 }
381 }364 }
382- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div><p class=\"p-title\">")365+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><p class=\"p-title\">")
383 if templ_7745c5c3_Err != nil {366 if templ_7745c5c3_Err != nil {
384 return templ_7745c5c3_Err367 return templ_7745c5c3_Err
385 }368 }
386- var templ_7745c5c3_Var19 string369+ var templ_7745c5c3_Var18 string
387- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)370+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
388 if templ_7745c5c3_Err != nil {371 if templ_7745c5c3_Err != nil {
389- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 58, Col: 34}372+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 57, Col: 34}
390 }373 }
391- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))374+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
392 if templ_7745c5c3_Err != nil {375 if templ_7745c5c3_Err != nil {
393 return templ_7745c5c3_Err376 return templ_7745c5c3_Err
394 }377 }
395- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p>")378+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p>")
396 if templ_7745c5c3_Err != nil {379 if templ_7745c5c3_Err != nil {
397 return templ_7745c5c3_Err380 return templ_7745c5c3_Err
398 }381 }
399 if m.Preview != "" {382 if m.Preview != "" {
400- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p class=\"p-body\">")383+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"p-body\">")
401 if templ_7745c5c3_Err != nil {384 if templ_7745c5c3_Err != nil {
402 return templ_7745c5c3_Err385 return templ_7745c5c3_Err
403 }386 }
404- var templ_7745c5c3_Var20 string387+ var templ_7745c5c3_Var19 string
405- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(m.Preview)388+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.Preview)
406 if templ_7745c5c3_Err != nil {389 if templ_7745c5c3_Err != nil {
407- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 60, Col: 36}390+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 59, Col: 36}
408 }391 }
409- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))392+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
410 if templ_7745c5c3_Err != nil {393 if templ_7745c5c3_Err != nil {
411 return templ_7745c5c3_Err394 return templ_7745c5c3_Err
412 }395 }
413- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</p>")396+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p>")
414 if templ_7745c5c3_Err != nil {397 if templ_7745c5c3_Err != nil {
415 return templ_7745c5c3_Err398 return templ_7745c5c3_Err
416 }399 }
417 }400 }
418 if m.Approve != "" {401 if m.Approve != "" {
419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"p-btns\"><form method=\"post\" action=\"")402+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"p-btns\"><form method=\"post\" action=\"")
420 if templ_7745c5c3_Err != nil {403 if templ_7745c5c3_Err != nil {
421 return templ_7745c5c3_Err404 return templ_7745c5c3_Err
422 }405 }
423- var templ_7745c5c3_Var21 templ.SafeURL406+ var templ_7745c5c3_Var20 templ.SafeURL
424- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Approve))407+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Approve))
425 if templ_7745c5c3_Err != nil {408 if templ_7745c5c3_Err != nil {
426- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 64, Col: 61}409+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 63, Col: 61}
427 }410 }
428- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))411+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
429 if templ_7745c5c3_Err != nil {412 if templ_7745c5c3_Err != nil {
430 return templ_7745c5c3_Err413 return templ_7745c5c3_Err
431 }414 }
432- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><button type=\"submit\" class=\"btn primary\">Approve</button></form><form method=\"post\" action=\"")415+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\"><button type=\"submit\" class=\"btn primary\">Approve</button></form><form method=\"post\" action=\"")
433 if templ_7745c5c3_Err != nil {416 if templ_7745c5c3_Err != nil {
434 return templ_7745c5c3_Err417 return templ_7745c5c3_Err
435 }418 }
436- var templ_7745c5c3_Var22 templ.SafeURL419+ var templ_7745c5c3_Var21 templ.SafeURL
437- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Reject))420+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Reject))
438 if templ_7745c5c3_Err != nil {421 if templ_7745c5c3_Err != nil {
439- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 65, Col: 60}422+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 64, Col: 60}
440 }423 }
441- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))424+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
442 if templ_7745c5c3_Err != nil {425 if templ_7745c5c3_Err != nil {
443 return templ_7745c5c3_Err426 return templ_7745c5c3_Err
444 }427 }
445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"><button type=\"submit\" class=\"btn\">Reject</button></form><form method=\"post\" action=\"")428+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><button type=\"submit\" class=\"btn\">Reject</button></form><form method=\"post\" action=\"")
446 if templ_7745c5c3_Err != nil {429 if templ_7745c5c3_Err != nil {
447 return templ_7745c5c3_Err430 return templ_7745c5c3_Err
448 }431 }
449- var templ_7745c5c3_Var23 templ.SafeURL432+ var templ_7745c5c3_Var22 templ.SafeURL
450- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Ban))433+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Ban))
451 if templ_7745c5c3_Err != nil {434 if templ_7745c5c3_Err != nil {
452- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 66, Col: 57}435+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 65, Col: 57}
453 }436 }
454- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))437+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
455 if templ_7745c5c3_Err != nil {438 if templ_7745c5c3_Err != nil {
456 return templ_7745c5c3_Err439 return templ_7745c5c3_Err
457 }440 }
458- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"><button type=\"submit\" class=\"btn danger\">Reject and ban</button></form></div>")441+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"><button type=\"submit\" class=\"btn danger\">Reject and ban</button></form></div>")
459 if templ_7745c5c3_Err != nil {442 if templ_7745c5c3_Err != nil {
460 return templ_7745c5c3_Err443 return templ_7745c5c3_Err
461 }444 }
462 }445 }
463- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</li>")446+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</li>")
464 if templ_7745c5c3_Err != nil {447 if templ_7745c5c3_Err != nil {
465 return templ_7745c5c3_Err448 return templ_7745c5c3_Err
466 }449 }
467 }450 }
468- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</ul>")451+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</ul>")
469 if templ_7745c5c3_Err != nil {452 if templ_7745c5c3_Err != nil {
470 return templ_7745c5c3_Err453 return templ_7745c5c3_Err
471 }454 }
472 } else {455 } else {
473- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<p class=\"lede dim\">nothing awaits moderation.</p>")456+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<p class=\"lede dim\">nothing awaits moderation.</p>")
474 if templ_7745c5c3_Err != nil {457 if templ_7745c5c3_Err != nil {
475 return templ_7745c5c3_Err458 return templ_7745c5c3_Err
476 }459 }
477 }460 }
478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</section>")461+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</section>")
479 if templ_7745c5c3_Err != nil {462 if templ_7745c5c3_Err != nil {
480 return templ_7745c5c3_Err463 return templ_7745c5c3_Err
481 }464 }
@@ -499,25 +482,25 @@ func InboxMRs(p InboxPage) templ.Component {
499 }()482 }()
500 }483 }
501 ctx = templ.InitializeContext(ctx)484 ctx = templ.InitializeContext(ctx)
502- templ_7745c5c3_Var24 := templ.GetChildren(ctx)485+ templ_7745c5c3_Var23 := templ.GetChildren(ctx)
503- if templ_7745c5c3_Var24 == nil {486+ if templ_7745c5c3_Var23 == nil {
504- templ_7745c5c3_Var24 = templ.NopComponent487+ templ_7745c5c3_Var23 = templ.NopComponent
505 }488 }
506 ctx = templ.ClearChildren(ctx)489 ctx = templ.ClearChildren(ctx)
507- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<section id=\"inbox-mrs\" aria-labelledby=\"inbox-mrs-h\"><h2 id=\"inbox-mrs-h\" class=\"rule-h\"><span>Open merge requests</span><span class=\"count\">")490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<section id=\"inbox-mrs\" aria-labelledby=\"inbox-mrs-h\"><h2 id=\"inbox-mrs-h\" class=\"rule-h\"><span>Open merge requests</span><span class=\"count\">")
508 if templ_7745c5c3_Err != nil {491 if templ_7745c5c3_Err != nil {
509 return templ_7745c5c3_Err492 return templ_7745c5c3_Err
510 }493 }
511- var templ_7745c5c3_Var25 string494+ var templ_7745c5c3_Var24 string
512- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.MRs)))495+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.MRs)))
513 if templ_7745c5c3_Err != nil {496 if templ_7745c5c3_Err != nil {
514- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 80, Col: 108}497+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 79, Col: 108}
515 }498 }
516- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))499+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
517 if templ_7745c5c3_Err != nil {500 if templ_7745c5c3_Err != nil {
518 return templ_7745c5c3_Err501 return templ_7745c5c3_Err
519 }502 }
520- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span></h2>")503+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span></h2>")
521 if templ_7745c5c3_Err != nil {504 if templ_7745c5c3_Err != nil {
522 return templ_7745c5c3_Err505 return templ_7745c5c3_Err
523 }506 }
@@ -526,12 +509,12 @@ func InboxMRs(p InboxPage) templ.Component {
526 return templ_7745c5c3_Err509 return templ_7745c5c3_Err
527 }510 }
528 if len(p.MRs) == 0 {511 if len(p.MRs) == 0 {
529- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<p class=\"lede dim\">no open merge requests.</p>")512+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<p class=\"lede dim\">no open merge requests.</p>")
530 if templ_7745c5c3_Err != nil {513 if templ_7745c5c3_Err != nil {
531 return templ_7745c5c3_Err514 return templ_7745c5c3_Err
532 }515 }
533 }516 }
534- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</section>")517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</section>")
535 if templ_7745c5c3_Err != nil {518 if templ_7745c5c3_Err != nil {
536 return templ_7745c5c3_Err519 return templ_7745c5c3_Err
537 }520 }
@@ -555,25 +538,25 @@ func InboxIssues(p InboxPage) templ.Component {
555 }()538 }()
556 }539 }
557 ctx = templ.InitializeContext(ctx)540 ctx = templ.InitializeContext(ctx)
558- templ_7745c5c3_Var26 := templ.GetChildren(ctx)541+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
559- if templ_7745c5c3_Var26 == nil {542+ if templ_7745c5c3_Var25 == nil {
560- templ_7745c5c3_Var26 = templ.NopComponent543+ templ_7745c5c3_Var25 = templ.NopComponent
561 }544 }
562 ctx = templ.ClearChildren(ctx)545 ctx = templ.ClearChildren(ctx)
563- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<section id=\"inbox-issues\" aria-labelledby=\"inbox-issues-h\"><h2 id=\"inbox-issues-h\" class=\"rule-h\"><span>Open issues</span><span class=\"count\">")546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<section id=\"inbox-issues\" aria-labelledby=\"inbox-issues-h\"><h2 id=\"inbox-issues-h\" class=\"rule-h\"><span>Open issues</span><span class=\"count\">")
564 if templ_7745c5c3_Err != nil {547 if templ_7745c5c3_Err != nil {
565 return templ_7745c5c3_Err548 return templ_7745c5c3_Err
566 }549 }
567- var templ_7745c5c3_Var27 string550+ var templ_7745c5c3_Var26 string
568- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.IssueRows)))551+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.IssueRows)))
569 if templ_7745c5c3_Err != nil {552 if templ_7745c5c3_Err != nil {
570- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 90, Col: 109}553+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 89, Col: 109}
571 }554 }
572- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))555+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
573 if templ_7745c5c3_Err != nil {556 if templ_7745c5c3_Err != nil {
574 return templ_7745c5c3_Err557 return templ_7745c5c3_Err
575 }558 }
576- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</span></h2>")559+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span></h2>")
577 if templ_7745c5c3_Err != nil {560 if templ_7745c5c3_Err != nil {
578 return templ_7745c5c3_Err561 return templ_7745c5c3_Err
579 }562 }
@@ -582,12 +565,12 @@ func InboxIssues(p InboxPage) templ.Component {
582 return templ_7745c5c3_Err565 return templ_7745c5c3_Err
583 }566 }
584 if len(p.IssueRows) == 0 {567 if len(p.IssueRows) == 0 {
585- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<p class=\"lede dim\">no open issues.</p>")568+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<p class=\"lede dim\">no open issues.</p>")
586 if templ_7745c5c3_Err != nil {569 if templ_7745c5c3_Err != nil {
587 return templ_7745c5c3_Err570 return templ_7745c5c3_Err
588 }571 }
589 }572 }
590- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</section>")573+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</section>")
591 if templ_7745c5c3_Err != nil {574 if templ_7745c5c3_Err != nil {
592 return templ_7745c5c3_Err575 return templ_7745c5c3_Err
593 }576 }
@@ -611,160 +594,160 @@ func inboxRows(rows []InboxRow, label string) templ.Component {
611 }()594 }()
612 }595 }
613 ctx = templ.InitializeContext(ctx)596 ctx = templ.InitializeContext(ctx)
614- templ_7745c5c3_Var28 := templ.GetChildren(ctx)597+ templ_7745c5c3_Var27 := templ.GetChildren(ctx)
615- if templ_7745c5c3_Var28 == nil {598+ if templ_7745c5c3_Var27 == nil {
616- templ_7745c5c3_Var28 = templ.NopComponent599+ templ_7745c5c3_Var27 = templ.NopComponent
617 }600 }
618 ctx = templ.ClearChildren(ctx)601 ctx = templ.ClearChildren(ctx)
619 if len(rows) > 0 {602 if len(rows) > 0 {
620- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<table class=\"list mrs\" aria-label=\"")603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<table class=\"list mrs\" aria-label=\"")
621 if templ_7745c5c3_Err != nil {604 if templ_7745c5c3_Err != nil {
622 return templ_7745c5c3_Err605 return templ_7745c5c3_Err
623 }606 }
624- var templ_7745c5c3_Var29 string607+ var templ_7745c5c3_Var28 string
625- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(label)608+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(label)
626 if templ_7745c5c3_Err != nil {609 if templ_7745c5c3_Err != nil {
627- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 100, Col: 44}610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 99, Col: 44}
628 }611 }
629- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
630 if templ_7745c5c3_Err != nil {613 if templ_7745c5c3_Err != nil {
631 return templ_7745c5c3_Err614 return templ_7745c5c3_Err
632 }615 }
633- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"><tbody data-rows>")616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"><tbody data-rows>")
634 if templ_7745c5c3_Err != nil {617 if templ_7745c5c3_Err != nil {
635 return templ_7745c5c3_Err618 return templ_7745c5c3_Err
636 }619 }
637 for _, m := range rows {620 for _, m := range rows {
638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<tr")621+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<tr")
639 if templ_7745c5c3_Err != nil {622 if templ_7745c5c3_Err != nil {
640 return templ_7745c5c3_Err623 return templ_7745c5c3_Err
641 }624 }
642 if m.Cursor {625 if m.Cursor {
643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " data-cursor")626+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " data-cursor")
644 if templ_7745c5c3_Err != nil {627 if templ_7745c5c3_Err != nil {
645 return templ_7745c5c3_Err628 return templ_7745c5c3_Err
646 }629 }
647 }630 }
648- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " data-state=\"open\"><td class=\"c-n mute\">")631+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " data-state=\"open\"><td class=\"c-n mute\">")
649 if templ_7745c5c3_Err != nil {632 if templ_7745c5c3_Err != nil {
650 return templ_7745c5c3_Err633 return templ_7745c5c3_Err
651 }634 }
652- var templ_7745c5c3_Var30 string635+ var templ_7745c5c3_Var29 string
653- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number)636+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number)
654 if templ_7745c5c3_Err != nil {637 if templ_7745c5c3_Err != nil {
655- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 37}638+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 103, Col: 37}
656 }639 }
657- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))640+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
658 if templ_7745c5c3_Err != nil {641 if templ_7745c5c3_Err != nil {
659 return templ_7745c5c3_Err642 return templ_7745c5c3_Err
660 }643 }
661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</td><td class=\"c-subject\"><a href=\"")644+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</td><td class=\"c-subject\"><a href=\"")
662 if templ_7745c5c3_Err != nil {645 if templ_7745c5c3_Err != nil {
663 return templ_7745c5c3_Err646 return templ_7745c5c3_Err
664 }647 }
665- var templ_7745c5c3_Var31 templ.SafeURL648+ var templ_7745c5c3_Var30 templ.SafeURL
666- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href))649+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href))
667 if templ_7745c5c3_Err != nil {650 if templ_7745c5c3_Err != nil {
668- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 59}651+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 59}
669 }652 }
670- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))653+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
671 if templ_7745c5c3_Err != nil {654 if templ_7745c5c3_Err != nil {
672 return templ_7745c5c3_Err655 return templ_7745c5c3_Err
673 }656 }
674- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">")657+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\">")
675 if templ_7745c5c3_Err != nil {658 if templ_7745c5c3_Err != nil {
676 return templ_7745c5c3_Err659 return templ_7745c5c3_Err
677 }660 }
678- var templ_7745c5c3_Var32 string661+ var templ_7745c5c3_Var31 string
679- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)662+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
680 if templ_7745c5c3_Err != nil {663 if templ_7745c5c3_Err != nil {
681- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 71}664+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 71}
682 }665 }
683- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))666+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
684 if templ_7745c5c3_Err != nil {667 if templ_7745c5c3_Err != nil {
685 return templ_7745c5c3_Err668 return templ_7745c5c3_Err
686 }669 }
687- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</a> <a class=\"mute\" href=\"")670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</a> <a class=\"mute\" href=\"")
688 if templ_7745c5c3_Err != nil {671 if templ_7745c5c3_Err != nil {
689 return templ_7745c5c3_Err672 return templ_7745c5c3_Err
690 }673 }
691- var templ_7745c5c3_Var33 templ.SafeURL674+ var templ_7745c5c3_Var32 templ.SafeURL
692- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))675+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.RepoHref))
693 if templ_7745c5c3_Err != nil {676 if templ_7745c5c3_Err != nil {
694- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 126}677+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 126}
695 }678 }
696- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))679+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
697 if templ_7745c5c3_Err != nil {680 if templ_7745c5c3_Err != nil {
698 return templ_7745c5c3_Err681 return templ_7745c5c3_Err
699 }682 }
700- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">")683+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">")
701 if templ_7745c5c3_Err != nil {684 if templ_7745c5c3_Err != nil {
702 return templ_7745c5c3_Err685 return templ_7745c5c3_Err
703 }686 }
704- var templ_7745c5c3_Var34 string687+ var templ_7745c5c3_Var33 string
705- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)688+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(m.Repo)
706 if templ_7745c5c3_Err != nil {689 if templ_7745c5c3_Err != nil {
707- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 105, Col: 137}690+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 104, Col: 137}
708 }691 }
709- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))692+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
710 if templ_7745c5c3_Err != nil {693 if templ_7745c5c3_Err != nil {
711 return templ_7745c5c3_Err694 return templ_7745c5c3_Err
712 }695 }
713- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</a></td><td class=\"c-when nowrap dim\">")696+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</a></td><td class=\"c-when nowrap dim\">")
714 if templ_7745c5c3_Err != nil {697 if templ_7745c5c3_Err != nil {
715 return templ_7745c5c3_Err698 return templ_7745c5c3_Err
716 }699 }
717 if m.WhenAt != "" {700 if m.WhenAt != "" {
718- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<time datetime=\"")701+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<time datetime=\"")
719 if templ_7745c5c3_Err != nil {702 if templ_7745c5c3_Err != nil {
720 return templ_7745c5c3_Err703 return templ_7745c5c3_Err
721 }704 }
722- var templ_7745c5c3_Var35 string705+ var templ_7745c5c3_Var34 string
723- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)706+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
724 if templ_7745c5c3_Err != nil {707 if templ_7745c5c3_Err != nil {
725- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 108, Col: 33}708+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 107, Col: 33}
726 }709 }
727- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)710+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
728 if templ_7745c5c3_Err != nil {711 if templ_7745c5c3_Err != nil {
729 return templ_7745c5c3_Err712 return templ_7745c5c3_Err
730 }713 }
731- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" title=\"")714+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" title=\"")
732 if templ_7745c5c3_Err != nil {715 if templ_7745c5c3_Err != nil {
733 return templ_7745c5c3_Err716 return templ_7745c5c3_Err
734 }717 }
735- var templ_7745c5c3_Var36 string718+ var templ_7745c5c3_Var35 string
736- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)719+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
737 if templ_7745c5c3_Err != nil {720 if templ_7745c5c3_Err != nil {
738- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 108, Col: 52}721+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 107, Col: 52}
739 }722 }
740- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)723+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
741 if templ_7745c5c3_Err != nil {724 if templ_7745c5c3_Err != nil {
742 return templ_7745c5c3_Err725 return templ_7745c5c3_Err
743 }726 }
744- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\">")727+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\">")
745 if templ_7745c5c3_Err != nil {728 if templ_7745c5c3_Err != nil {
746 return templ_7745c5c3_Err729 return templ_7745c5c3_Err
747 }730 }
748- var templ_7745c5c3_Var37 string731+ var templ_7745c5c3_Var36 string
749- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)732+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
750 if templ_7745c5c3_Err != nil {733 if templ_7745c5c3_Err != nil {
751- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 108, Col: 63}734+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 107, Col: 63}
752 }735 }
753- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))736+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
754 if templ_7745c5c3_Err != nil {737 if templ_7745c5c3_Err != nil {
755 return templ_7745c5c3_Err738 return templ_7745c5c3_Err
756 }739 }
757- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</time>")740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</time>")
758 if templ_7745c5c3_Err != nil {741 if templ_7745c5c3_Err != nil {
759 return templ_7745c5c3_Err742 return templ_7745c5c3_Err
760 }743 }
761 }744 }
762- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</td></tr>")745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</td></tr>")
763 if templ_7745c5c3_Err != nil {746 if templ_7745c5c3_Err != nil {
764 return templ_7745c5c3_Err747 return templ_7745c5c3_Err
765 }748 }
766 }749 }
767- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</tbody></table>")750+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</tbody></table>")
768 if templ_7745c5c3_Err != nil {751 if templ_7745c5c3_Err != nil {
769 return templ_7745c5c3_Err752 return templ_7745c5c3_Err
770 }753 }
@@ -773,265 +756,4 @@ func inboxRows(rows []InboxRow, label string) templ.Component {
773 })756 })
774 }757 }
775 758
776-// InboxRuns stays as an empty fragment when CI is disabled.
777-func InboxRuns(p InboxPage) templ.Component {
778- return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
779- templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
780- if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
781- return templ_7745c5c3_CtxErr
782- }
783- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
784- if !templ_7745c5c3_IsBuffer {
785- defer func() {
786- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
787- if templ_7745c5c3_Err == nil {
788- templ_7745c5c3_Err = templ_7745c5c3_BufErr
789- }
790- }()
791- }
792- ctx = templ.InitializeContext(ctx)
793- templ_7745c5c3_Var38 := templ.GetChildren(ctx)
794- if templ_7745c5c3_Var38 == nil {
795- templ_7745c5c3_Var38 = templ.NopComponent
796- }
797- ctx = templ.ClearChildren(ctx)
798- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<section id=\"inbox-runs\"")
799- if templ_7745c5c3_Err != nil {
800- return templ_7745c5c3_Err
801- }
802- if !p.RunsEnabled {
803- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " hidden")
804- if templ_7745c5c3_Err != nil {
805- return templ_7745c5c3_Err
806- }
807- }
808- if p.RunsEnabled {
809- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " aria-labelledby=\"inbox-runs-h\"")
810- if templ_7745c5c3_Err != nil {
811- return templ_7745c5c3_Err
812- }
813- }
814- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, ">")
815- if templ_7745c5c3_Err != nil {
816- return templ_7745c5c3_Err
817- }
818- if p.RunsEnabled {
819- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<h2 id=\"inbox-runs-h\" class=\"rule-h\"><span>Running</span><span class=\"count\">")
820- if templ_7745c5c3_Err != nil {
821- return templ_7745c5c3_Err
822- }
823- var templ_7745c5c3_Var39 string
824- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Runs)))
825- if templ_7745c5c3_Err != nil {
826- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 126, Col: 99}
827- }
828- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
829- if templ_7745c5c3_Err != nil {
830- return templ_7745c5c3_Err
831- }
832- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span></h2>")
833- if templ_7745c5c3_Err != nil {
834- return templ_7745c5c3_Err
835- }
836- if len(p.Runs) == 0 {
837- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<p class=\"lede dim\">no runs in progress.</p>")
838- if templ_7745c5c3_Err != nil {
839- return templ_7745c5c3_Err
840- }
841- } else {
842- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<table class=\"list mrs\" aria-label=\"Running\"><tbody data-rows>")
843- if templ_7745c5c3_Err != nil {
844- return templ_7745c5c3_Err
845- }
846- for _, r := range p.Runs {
847- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<tr><td class=\"c-st\">")
848- if templ_7745c5c3_Err != nil {
849- return templ_7745c5c3_Err
850- }
851- templ_7745c5c3_Err = CheckIcon(r.Kind, r.State).Render(ctx, templ_7745c5c3_Buffer)
852- if templ_7745c5c3_Err != nil {
853- return templ_7745c5c3_Err
854- }
855- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<span class=\"dim\">")
856- if templ_7745c5c3_Err != nil {
857- return templ_7745c5c3_Err
858- }
859- var templ_7745c5c3_Var40 string
860- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
861- if templ_7745c5c3_Err != nil {
862- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 134, Col: 79}
863- }
864- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
865- if templ_7745c5c3_Err != nil {
866- return templ_7745c5c3_Err
867- }
868- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</span></td><td class=\"c-subject\"><a href=\"")
869- if templ_7745c5c3_Err != nil {
870- return templ_7745c5c3_Err
871- }
872- var templ_7745c5c3_Var41 templ.SafeURL
873- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
874- if templ_7745c5c3_Err != nil {
875- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 61}
876- }
877- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
878- if templ_7745c5c3_Err != nil {
879- return templ_7745c5c3_Err
880- }
881- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\">")
882- if templ_7745c5c3_Err != nil {
883- return templ_7745c5c3_Err
884- }
885- var templ_7745c5c3_Var42 string
886- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(r.Workflow)
887- if templ_7745c5c3_Err != nil {
888- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 76}
889- }
890- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
891- if templ_7745c5c3_Err != nil {
892- return templ_7745c5c3_Err
893- }
894- var templ_7745c5c3_Var43 string
895- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
896- if templ_7745c5c3_Err != nil {
897- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 83}
898- }
899- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
900- if templ_7745c5c3_Err != nil {
901- return templ_7745c5c3_Err
902- }
903- var templ_7745c5c3_Var44 string
904- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
905- if templ_7745c5c3_Err != nil {
906- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 95}
907- }
908- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
909- if templ_7745c5c3_Err != nil {
910- return templ_7745c5c3_Err
911- }
912- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</a> <a class=\"mute\" href=\"")
913- if templ_7745c5c3_Err != nil {
914- return templ_7745c5c3_Err
915- }
916- var templ_7745c5c3_Var45 templ.SafeURL
917- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.RepoHref))
918- if templ_7745c5c3_Err != nil {
919- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 150}
920- }
921- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
922- if templ_7745c5c3_Err != nil {
923- return templ_7745c5c3_Err
924- }
925- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\">")
926- if templ_7745c5c3_Err != nil {
927- return templ_7745c5c3_Err
928- }
929- var templ_7745c5c3_Var46 string
930- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(r.Repo)
931- if templ_7745c5c3_Err != nil {
932- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 161}
933- }
934- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
935- if templ_7745c5c3_Err != nil {
936- return templ_7745c5c3_Err
937- }
938- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</a> <span class=\"dim\">")
939- if templ_7745c5c3_Err != nil {
940- return templ_7745c5c3_Err
941- }
942- var templ_7745c5c3_Var47 string
943- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(r.Ref)
944- if templ_7745c5c3_Err != nil {
945- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 193}
946- }
947- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
948- if templ_7745c5c3_Err != nil {
949- return templ_7745c5c3_Err
950- }
951- var templ_7745c5c3_Var48 string
952- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
953- if templ_7745c5c3_Err != nil {
954- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 200}
955- }
956- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
957- if templ_7745c5c3_Err != nil {
958- return templ_7745c5c3_Err
959- }
960- var templ_7745c5c3_Var49 string
961- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(r.Event)
962- if templ_7745c5c3_Err != nil {
963- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 135, Col: 211}
964- }
965- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
966- if templ_7745c5c3_Err != nil {
967- return templ_7745c5c3_Err
968- }
969- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span></td><td class=\"c-when nowrap dim\">")
970- if templ_7745c5c3_Err != nil {
971- return templ_7745c5c3_Err
972- }
973- if r.WhenAt != "" {
974- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<time datetime=\"")
975- if templ_7745c5c3_Err != nil {
976- return templ_7745c5c3_Err
977- }
978- var templ_7745c5c3_Var50 string
979- templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
980- if templ_7745c5c3_Err != nil {
981- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 138, Col: 35}
982- }
983- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
984- if templ_7745c5c3_Err != nil {
985- return templ_7745c5c3_Err
986- }
987- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" title=\"")
988- if templ_7745c5c3_Err != nil {
989- return templ_7745c5c3_Err
990- }
991- var templ_7745c5c3_Var51 string
992- templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
993- if templ_7745c5c3_Err != nil {
994- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 138, Col: 54}
995- }
996- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
997- if templ_7745c5c3_Err != nil {
998- return templ_7745c5c3_Err
999- }
1000- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">")
1001- if templ_7745c5c3_Err != nil {
1002- return templ_7745c5c3_Err
1003- }
1004- var templ_7745c5c3_Var52 string
1005- templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
1006- if templ_7745c5c3_Err != nil {
1007- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/inbox.templ`, Line: 138, Col: 65}
1008- }
1009- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
1010- if templ_7745c5c3_Err != nil {
1011- return templ_7745c5c3_Err
1012- }
1013- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</time>")
1014- if templ_7745c5c3_Err != nil {
1015- return templ_7745c5c3_Err
1016- }
1017- }
1018- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</td></tr>")
1019- if templ_7745c5c3_Err != nil {
1020- return templ_7745c5c3_Err
1021- }
1022- }
1023- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</tbody></table>")
1024- if templ_7745c5c3_Err != nil {
1025- return templ_7745c5c3_Err
1026- }
1027- }
1028- }
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</section>")
1030- if templ_7745c5c3_Err != nil {
1031- return templ_7745c5c3_Err
1032- }
1033- return nil
1034- })
1035-}
1036-
1037 var _ = templruntime.GeneratedTemplate759 var _ = templruntime.GeneratedTemplate
M internal/adapters/web/inbox_test.go +4 -6
@@ -180,13 +180,12 @@ func TestInboxLiveFragments(t *testing.T) {
180180 ls := &liveStream{s: srv, view: "inbox"}
181181 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
182182 require.NoError(t, err)
183- require.Len(t, frags, 6)
183+ require.Len(t, frags, 5)
184184 assert.Contains(t, frags[0].html, `id="inbox-counts"`)
185185 assert.Contains(t, frags[1].html, `id="inbox-repos"`)
186186 assert.Contains(t, frags[2].html, `id="inbox-pending"`)
187- assert.Contains(t, frags[3].html, `id="inbox-runs"`)
188- assert.Contains(t, frags[4].html, `id="inbox-mrs"`)
189- assert.Contains(t, frags[5].html, `id="inbox-issues"`)
187+ assert.Contains(t, frags[3].html, `id="inbox-mrs"`)
188+ assert.Contains(t, frags[4].html, `id="inbox-issues"`)
190189
191190 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
192191 require.NoError(t, err)
@@ -287,7 +286,6 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
287286 t.Parallel()
288287 for name, ev := range map[string]domain.Event{
289288 "issues turned off": domain.RepoUpdated{Repo: repoName(t, "beta")},
290- "run updated": domain.RunUpdated{},
291289 "repo deleted": domain.RepoDeleted{Repo: repoName(t, "beta")},
292290 } {
293291 t.Run(name, func(t *testing.T) {
@@ -299,7 +297,7 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
299297 ls := &liveStream{s: srv, view: "inbox"}
300298 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: ev})
301299 require.NoError(t, err)
302- require.Len(t, frags, 6)
300+ require.Len(t, frags, 5)
303301 all := ""
304302 for _, f := range frags {
305303 all += f.html
@@ -180,13 +180,12 @@ func TestInboxLiveFragments(t *testing.T) {
180 ls := &liveStream{s: srv, view: "inbox"}180 ls := &liveStream{s: srv, view: "inbox"}
181 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})181 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
182 require.NoError(t, err)182 require.NoError(t, err)
183- require.Len(t, frags, 6)183+ require.Len(t, frags, 5)
184 assert.Contains(t, frags[0].html, `id="inbox-counts"`)184 assert.Contains(t, frags[0].html, `id="inbox-counts"`)
185 assert.Contains(t, frags[1].html, `id="inbox-repos"`)185 assert.Contains(t, frags[1].html, `id="inbox-repos"`)
186 assert.Contains(t, frags[2].html, `id="inbox-pending"`)186 assert.Contains(t, frags[2].html, `id="inbox-pending"`)
187- assert.Contains(t, frags[3].html, `id="inbox-runs"`)187+ assert.Contains(t, frags[3].html, `id="inbox-mrs"`)
188- assert.Contains(t, frags[4].html, `id="inbox-mrs"`)188+ assert.Contains(t, frags[4].html, `id="inbox-issues"`)
189- assert.Contains(t, frags[5].html, `id="inbox-issues"`)
190 189
191 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})190 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
192 require.NoError(t, err)191 require.NoError(t, err)
@@ -287,7 +286,6 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
287 t.Parallel()286 t.Parallel()
288 for name, ev := range map[string]domain.Event{287 for name, ev := range map[string]domain.Event{
289 "issues turned off": domain.RepoUpdated{Repo: repoName(t, "beta")},288 "issues turned off": domain.RepoUpdated{Repo: repoName(t, "beta")},
290- "run updated": domain.RunUpdated{},
291 "repo deleted": domain.RepoDeleted{Repo: repoName(t, "beta")},289 "repo deleted": domain.RepoDeleted{Repo: repoName(t, "beta")},
292 } {290 } {
293 t.Run(name, func(t *testing.T) {291 t.Run(name, func(t *testing.T) {
@@ -299,7 +297,7 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
299 ls := &liveStream{s: srv, view: "inbox"}297 ls := &liveStream{s: srv, view: "inbox"}
300 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: ev})298 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: ev})
301 require.NoError(t, err)299 require.NoError(t, err)
302- require.Len(t, frags, 6)300+ require.Len(t, frags, 5)
303 all := ""301 all := ""
304 for _, f := range frags {302 for _, f := range frags {
305 all += f.html303 all += f.html
D internal/adapters/web/jstest/keys.test.mjs +0 -29
@@ -1,29 +0,0 @@
1-// Exercise the production keydown handler without loading the rest of the page.
2-import assert from 'node:assert/strict';
3-import { readFileSync } from 'node:fs';
4-import { test } from 'node:test';
5-const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6-const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7-
8-function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9- let handler;
10- const fired = [];
11- const window = { ermiteKeys: { blocks: () => off } };
12- const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13- new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14- return (key) => {
15- const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16- handler(e);
17- return fired;
18- };
19-}
20-test('Shift+I opens inbox while lowercase i retains the issues tab', () => {
21- const press = page();
22- assert.deepEqual(press('I'), ['inbox']);
23- assert.deepEqual(press('i'), ['inbox', 'i']);
24-});
25-test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
26- for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
27- assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
28- }
29-});
@@ -1,29 +0,0 @@
1-// Exercise the production keydown handler without loading the rest of the page.
2-import assert from 'node:assert/strict';
3-import { readFileSync } from 'node:fs';
4-import { test } from 'node:test';
5-const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6-const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7-
8-function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9- let handler;
10- const fired = [];
11- const window = { ermiteKeys: { blocks: () => off } };
12- const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13- new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14- return (key) => {
15- const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16- handler(e);
17- return fired;
18- };
19-}
20-test('Shift+I opens inbox while lowercase i retains the issues tab', () => {
21- const press = page();
22- assert.deepEqual(press('I'), ['inbox']);
23- assert.deepEqual(press('i'), ['inbox', 'i']);
24-});
25-test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
26- for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
27- assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
28- }
29-});
M internal/adapters/web/jstest/mr.test.mjs +12 -4
@@ -152,8 +152,10 @@ test('footer tabs: closed by default, toggle, switch, arrows, hash', () => {
152152 test('footer: #mr-ck syncs the checks label; its click opens Checks without toggling', () => {
153153 const text0 = { v: '' }; let writes = 0;
154154 const label = { get textContent() { return text0.v; }, set textContent(v) { writes++; text0.v = v; } };
155- const summary = { title: '1/2 failed', textContent: ' 1/2 ', parentElement: {} };
156- const h = footerHarness({ summary });
155+ const bar = {};
156+ const summary = { title: '1/2 failed', textContent: ' 1/2 ', closest: (s) => s === '.mr-bar' ? bar : null };
157+ const extra = { summary };
158+ const h = footerHarness(extra);
157159 let observed = null, callback = null;
158160 globalThis.MutationObserver = class { constructor(fn) { callback = fn; } observe(target, opts) { observed = [target, opts]; } };
159161 const nav = document.querySelector('[data-mr-tabs]');
@@ -161,8 +163,14 @@ test('footer: #mr-ck syncs the checks label; its click opens Checks without togg
161163 try {
162164 window.ermiteMR.panels();
163165 assert.equal(label.textContent, '1/2 failed', 'initial label sync');
164- assert.equal(observed[0], summary.parentElement);
165- assert.deepEqual(observed[1], { childList: true }, 'only #mr-ck replacements wake the observer');
166+ assert.equal(observed[0], bar);
167+ assert.deepEqual(observed[1], { childList: true, subtree: true }, 'the stable bar observes action and badge replacements');
168+ // Replace #mr-actions, including its badge, then replace the badge alone.
169+ extra.summary = { title: 'checks running', textContent: '' }; callback();
170+ assert.equal(label.textContent, 'checks running');
171+ extra.summary = { title: 'checks passed', textContent: '' }; callback();
172+ assert.equal(label.textContent, 'checks passed');
173+ extra.summary = summary; callback();
166174 const before = writes; callback(); callback();
167175 assert.equal(writes, before, 'an unchanged label is not written');
168176 summary.title = ''; callback();
@@ -152,8 +152,10 @@ test('footer tabs: closed by default, toggle, switch, arrows, hash', () => {
152 test('footer: #mr-ck syncs the checks label; its click opens Checks without toggling', () => {152 test('footer: #mr-ck syncs the checks label; its click opens Checks without toggling', () => {
153 const text0 = { v: '' }; let writes = 0;153 const text0 = { v: '' }; let writes = 0;
154 const label = { get textContent() { return text0.v; }, set textContent(v) { writes++; text0.v = v; } };154 const label = { get textContent() { return text0.v; }, set textContent(v) { writes++; text0.v = v; } };
155- const summary = { title: '1/2 failed', textContent: ' 1/2 ', parentElement: {} };155+ const bar = {};
156- const h = footerHarness({ summary });156+ const summary = { title: '1/2 failed', textContent: ' 1/2 ', closest: (s) => s === '.mr-bar' ? bar : null };
157+ const extra = { summary };
158+ const h = footerHarness(extra);
157 let observed = null, callback = null;159 let observed = null, callback = null;
158 globalThis.MutationObserver = class { constructor(fn) { callback = fn; } observe(target, opts) { observed = [target, opts]; } };160 globalThis.MutationObserver = class { constructor(fn) { callback = fn; } observe(target, opts) { observed = [target, opts]; } };
159 const nav = document.querySelector('[data-mr-tabs]');161 const nav = document.querySelector('[data-mr-tabs]');
@@ -161,8 +163,14 @@ test('footer: #mr-ck syncs the checks label; its click opens Checks without togg
161 try {163 try {
162 window.ermiteMR.panels();164 window.ermiteMR.panels();
163 assert.equal(label.textContent, '1/2 failed', 'initial label sync');165 assert.equal(label.textContent, '1/2 failed', 'initial label sync');
164- assert.equal(observed[0], summary.parentElement);166+ assert.equal(observed[0], bar);
165- assert.deepEqual(observed[1], { childList: true }, 'only #mr-ck replacements wake the observer');167+ assert.deepEqual(observed[1], { childList: true, subtree: true }, 'the stable bar observes action and badge replacements');
168+ // Replace #mr-actions, including its badge, then replace the badge alone.
169+ extra.summary = { title: 'checks running', textContent: '' }; callback();
170+ assert.equal(label.textContent, 'checks running');
171+ extra.summary = { title: 'checks passed', textContent: '' }; callback();
172+ assert.equal(label.textContent, 'checks passed');
173+ extra.summary = summary; callback();
166 const before = writes; callback(); callback();174 const before = writes; callback(); callback();
167 assert.equal(writes, before, 'an unchanged label is not written');175 assert.equal(writes, before, 'an unchanged label is not written');
168 summary.title = ''; callback();176 summary.title = ''; callback();
M internal/adapters/web/keys.templ +0 -1
@@ -28,7 +28,6 @@ templ Keys() {
2828 k-r from:body -> ermite.tab('r');
2929 k-m from:body -> ermite.tab('m');
3030 k-i from:body -> ermite.tab('i');
31- k-inbox from:body -> ermite.go('[data-inbox]');
3231 k-next from:body -> ermite.nextNote();
3332 k-split from:body -> ermite.layout('split');
3433 k-unified from:body -> ermite.layout('unified');
@@ -28,7 +28,6 @@ templ Keys() {
28 k-r from:body -> ermite.tab('r');28 k-r from:body -> ermite.tab('r');
29 k-m from:body -> ermite.tab('m');29 k-m from:body -> ermite.tab('m');
30 k-i from:body -> ermite.tab('i');30 k-i from:body -> ermite.tab('i');
31- k-inbox from:body -> ermite.go('[data-inbox]');
32 k-next from:body -> ermite.nextNote();31 k-next from:body -> ermite.nextNote();
33 k-split from:body -> ermite.layout('split');32 k-split from:body -> ermite.layout('split');
34 k-unified from:body -> ermite.layout('unified');33 k-unified from:body -> ermite.layout('unified');
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/mr.go +7 -0
@@ -558,6 +558,10 @@ func (s *Server) mrSince(ctx context.Context, r *http.Request) (templ.Component,
558558 }
559559
560560 func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ.Component, int, error) {
561+ return s.mrPageView(ctx, r, since, false)
562+}
563+
564+func (s *Server) mrPageView(ctx context.Context, r *http.Request, since, fragment bool) (templ.Component, int, error) {
561565 if since && !SignedIn(ctx) {
562566 return nil, 0, errNotFound
563567 }
@@ -649,6 +653,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
649653 }
650654 }
651655
656+ if fragment {
657+ return MRStateFragment(p), http.StatusOK, nil
658+ }
652659 l := s.repoLayout(ctx, d, "merge-requests")
653660 l.Title = p.Number + " " + p.Title + " · " + d.name() + " · " + s.instance
654661 if head != "" {
@@ -558,6 +558,10 @@ func (s *Server) mrSince(ctx context.Context, r *http.Request) (templ.Component,
558 }558 }
559 559
560 func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ.Component, int, error) {560 func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ.Component, int, error) {
561+ return s.mrPageView(ctx, r, since, false)
562+}
563+
564+func (s *Server) mrPageView(ctx context.Context, r *http.Request, since, fragment bool) (templ.Component, int, error) {
561 if since && !SignedIn(ctx) {565 if since && !SignedIn(ctx) {
562 return nil, 0, errNotFound566 return nil, 0, errNotFound
563 }567 }
@@ -649,6 +653,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
649 }653 }
650 }654 }
651 655
656+ if fragment {
657+ return MRStateFragment(p), http.StatusOK, nil
658+ }
652 l := s.repoLayout(ctx, d, "merge-requests")659 l := s.repoLayout(ctx, d, "merge-requests")
653 l.Title = p.Number + " " + p.Title + " · " + d.name() + " · " + s.instance660 l.Title = p.Number + " " + p.Title + " · " + d.name() + " · " + s.instance
654 if head != "" {661 if head != "" {
M internal/adapters/web/mr.templ +62 -29
@@ -13,7 +13,7 @@ templ MRList(l Layout, p MRListPage) {
1313 @Shell(l, mrListRail(p)) {
1414 if p.Live {
1515 @LiveSlot("mr-notice")
16- <div id="mr-refresh-status" role="status" aria-live="polite"></div>
16+ <div id="mr-refresh-status" role="status" aria-live="polite"></div>
1717 }
1818 <div class="filter">
1919 <label for="mr-filter" class="sr">filter</label>
@@ -119,12 +119,7 @@ templ MergeRequest(l Layout, p MRPage) {
119119 <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
120120 <dl class="kv show-kv">
121121 <dt>state</dt>
122- <dd class="mr-state" data-state={ p.State }>
123- { p.State }
124- if p.MergedShort != "" {
125- { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }
126- }
127- </dd>
122+ @mrState(p)
128123 <dt>branch</dt>
129124 <dd>
130125 @branchLink(p.Source, p.SourceHref)
@@ -215,19 +210,19 @@ templ MergeRequest(l Layout, p MRPage) {
215210 }
216211 </div>
217212 <div id="mr-comments">
218- <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2>
219- for _, c := range p.Loose {
220- @commentView(c, "")
221- }
222- for _, c := range p.General {
223- @commentView(c, "")
224- }
225- if p.NComments == 0 {
226- <p class="dim">no comments yet.</p>
227- }
228- if p.GeneralForm != nil {
229- @commentForm(*p.GeneralForm, "")
230- }
213+ <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2>
214+ for _, c := range p.Loose {
215+ @commentView(c, "")
216+ }
217+ for _, c := range p.General {
218+ @commentView(c, "")
219+ }
220+ if p.NComments == 0 {
221+ <p class="dim">no comments yet.</p>
222+ }
223+ if p.GeneralForm != nil {
224+ @commentForm(*p.GeneralForm, "")
225+ }
231226 </div>
232227 }
233228 }
@@ -467,13 +462,7 @@ templ mrBar(p MRPage) {
467462 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">
468463 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
469464 @diffViewMenu(p.DiffView)
470- <div class="mr-acts">
471- @MRChecksSum(p.ChecksSum)
472- if p.Box != nil {
473- @mergeMenu(p)
474- }
475- @moreMenu(p)
476- </div>
465+ @mrActions(p, false)
477466 </div>
478467 }
479468
@@ -523,7 +512,15 @@ templ mergeMenu(p MRPage) {
523512 </div>
524513 </form>
525514 }
526- <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>
515+ <form
516+ class="d-acts"
517+ method="post"
518+ action={ templ.SafeURL(p.Box.MergeAction) }
519+ hx-post={ p.Box.MergeAction }
520+ hx-swap="none"
521+ hx-sync="this:drop"
522+ hx-disable="find button[type=submit]"
523+ >
527524 <input type="hidden" name="head_id" value={ p.Box.Head }/>
528525 <fieldset class="d-modes">
529526 <legend class="sr">merge mode</legend>
@@ -557,9 +554,10 @@ templ mergeMenu(p MRPage) {
557554 data-default
558555 disabled?={ !p.Box.CanMerge() }
559556 title={ p.Box.MergeTitle() }
560- :disabled="!this.form.querySelector('input[name=mode]:checked:not(:disabled)')"
557+ :disabled="this.form.classList.contains('htmx-request') || !this.form.querySelector('input[name=mode]:checked:not(:disabled)')"
561558 :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''"
562559 >Merge into { p.Target }</button>
560+ <span class="htmx-indicator" role="status">Merging…</span>
563561 </div>
564562 </form>
565563 if p.Box.CanAutoMerge {
@@ -779,3 +777,38 @@ templ MRMergeChecks(c ChecksSummary) {
779777 }
780778 </div>
781779 }
780+
781+// MRStateFragment updates only state and actions, leaving review drafts alone.
782+templ MRStateFragment(p MRPage) {
783+ @mrState(p)
784+ @mrActions(p, true)
785+}
786+
787+templ mrState(p MRPage) {
788+ <dd id="mr-state" hx-swap-oob="true" class="mr-state" data-state={ p.State }>
789+ { p.State }
790+ if p.MergedShort != "" {
791+ { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }
792+ }
793+ </dd>
794+}
795+
796+templ mrActions(p MRPage, fragment bool) {
797+ <div id="mr-actions" hx-swap-oob="true" class="mr-acts">
798+ @MRChecksSum(p.ChecksSum)
799+ if p.Box != nil {
800+ @mergeMenu(p)
801+ }
802+ @moreMenu(p)
803+ if fragment && p.MergeError != "" {
804+ <p class="d-err" role="alert">{ p.MergeError }</p>
805+ }
806+ </div>
807+}
808+
809+// MRActionStatus remains visible even when the request detail cannot be read.
810+templ MRActionStatus(message, href string) {
811+ <div id="mr-actions" class="mr-acts" hx-swap-oob="true">
812+ <p class="d-err" role="alert">{ message } <a href={ templ.SafeURL(href) }>reload</a></p>
813+ </div>
814+}
@@ -13,7 +13,7 @@ templ MRList(l Layout, p MRListPage) {
13 @Shell(l, mrListRail(p)) {13 @Shell(l, mrListRail(p)) {
14 if p.Live {14 if p.Live {
15 @LiveSlot("mr-notice")15 @LiveSlot("mr-notice")
16- <div id="mr-refresh-status" role="status" aria-live="polite"></div>16+ <div id="mr-refresh-status" role="status" aria-live="polite"></div>
17 }17 }
18 <div class="filter">18 <div class="filter">
19 <label for="mr-filter" class="sr">filter</label>19 <label for="mr-filter" class="sr">filter</label>
@@ -119,12 +119,7 @@ templ MergeRequest(l Layout, p MRPage) {
119 <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>119 <h1><span class="mute">{ p.Number }</span> { p.Title }</h1>
120 <dl class="kv show-kv">120 <dl class="kv show-kv">
121 <dt>state</dt>121 <dt>state</dt>
122- <dd class="mr-state" data-state={ p.State }>122+ @mrState(p)
123- { p.State }
124- if p.MergedShort != "" {
125- { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }
126- }
127- </dd>
128 <dt>branch</dt>123 <dt>branch</dt>
129 <dd>124 <dd>
130 @branchLink(p.Source, p.SourceHref)125 @branchLink(p.Source, p.SourceHref)
@@ -215,19 +210,19 @@ templ MergeRequest(l Layout, p MRPage) {
215 }210 }
216 </div>211 </div>
217 <div id="mr-comments">212 <div id="mr-comments">
218- <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2>213+ <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2>
219- for _, c := range p.Loose {214+ for _, c := range p.Loose {
220- @commentView(c, "")215+ @commentView(c, "")
221- }216+ }
222- for _, c := range p.General {217+ for _, c := range p.General {
223- @commentView(c, "")218+ @commentView(c, "")
224- }219+ }
225- if p.NComments == 0 {220+ if p.NComments == 0 {
226- <p class="dim">no comments yet.</p>221+ <p class="dim">no comments yet.</p>
227- }222+ }
228- if p.GeneralForm != nil {223+ if p.GeneralForm != nil {
229- @commentForm(*p.GeneralForm, "")224+ @commentForm(*p.GeneralForm, "")
230- }225+ }
231 </div>226 </div>
232 }227 }
233 }228 }
@@ -467,13 +462,7 @@ templ mrBar(p MRPage) {
467 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">462 <div class="diff-tools mr-bar" role="group" aria-label="Diff view and merge">
468 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>463 <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span>
469 @diffViewMenu(p.DiffView)464 @diffViewMenu(p.DiffView)
470- <div class="mr-acts">465+ @mrActions(p, false)
471- @MRChecksSum(p.ChecksSum)
472- if p.Box != nil {
473- @mergeMenu(p)
474- }
475- @moreMenu(p)
476- </div>
477 </div>466 </div>
478 }467 }
479 468
@@ -523,7 +512,15 @@ templ mergeMenu(p MRPage) {
523 </div>512 </div>
524 </form>513 </form>
525 }514 }
526- <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>515+ <form
516+ class="d-acts"
517+ method="post"
518+ action={ templ.SafeURL(p.Box.MergeAction) }
519+ hx-post={ p.Box.MergeAction }
520+ hx-swap="none"
521+ hx-sync="this:drop"
522+ hx-disable="find button[type=submit]"
523+ >
527 <input type="hidden" name="head_id" value={ p.Box.Head }/>524 <input type="hidden" name="head_id" value={ p.Box.Head }/>
528 <fieldset class="d-modes">525 <fieldset class="d-modes">
529 <legend class="sr">merge mode</legend>526 <legend class="sr">merge mode</legend>
@@ -557,9 +554,10 @@ templ mergeMenu(p MRPage) {
557 data-default554 data-default
558 disabled?={ !p.Box.CanMerge() }555 disabled?={ !p.Box.CanMerge() }
559 title={ p.Box.MergeTitle() }556 title={ p.Box.MergeTitle() }
560- :disabled="!this.form.querySelector('input[name=mode]:checked:not(:disabled)')"557+ :disabled="this.form.classList.contains('htmx-request') || !this.form.querySelector('input[name=mode]:checked:not(:disabled)')"
561 :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''"558 :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''"
562 >Merge into { p.Target }</button>559 >Merge into { p.Target }</button>
560+ <span class="htmx-indicator" role="status">Merging…</span>
563 </div>561 </div>
564 </form>562 </form>
565 if p.Box.CanAutoMerge {563 if p.Box.CanAutoMerge {
@@ -779,3 +777,38 @@ templ MRMergeChecks(c ChecksSummary) {
779 }777 }
780 </div>778 </div>
781 }779 }
780+
781+// MRStateFragment updates only state and actions, leaving review drafts alone.
782+templ MRStateFragment(p MRPage) {
783+ @mrState(p)
784+ @mrActions(p, true)
785+}
786+
787+templ mrState(p MRPage) {
788+ <dd id="mr-state" hx-swap-oob="true" class="mr-state" data-state={ p.State }>
789+ { p.State }
790+ if p.MergedShort != "" {
791+ { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode }
792+ }
793+ </dd>
794+}
795+
796+templ mrActions(p MRPage, fragment bool) {
797+ <div id="mr-actions" hx-swap-oob="true" class="mr-acts">
798+ @MRChecksSum(p.ChecksSum)
799+ if p.Box != nil {
800+ @mergeMenu(p)
801+ }
802+ @moreMenu(p)
803+ if fragment && p.MergeError != "" {
804+ <p class="d-err" role="alert">{ p.MergeError }</p>
805+ }
806+ </div>
807+}
808+
809+// MRActionStatus remains visible even when the request detail cannot be read.
810+templ MRActionStatus(message, href string) {
811+ <div id="mr-actions" class="mr-acts" hx-swap-oob="true">
812+ <p class="d-err" role="alert">{ message } <a href={ templ.SafeURL(href) }>reload</a></p>
813+ </div>
814+}
A internal/adapters/web/mr_async_test.go +166 -0
@@ -0,0 +1,166 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "context"
7+ "errors"
8+ "net/http"
9+ "net/http/httptest"
10+ "strings"
11+ "testing"
12+ "time"
13+
14+ "github.com/stretchr/testify/assert"
15+ "github.com/stretchr/testify/mock"
16+ "github.com/stretchr/testify/require"
17+
18+ "bnema.dev/ermite/internal/domain"
19+ "bnema.dev/ermite/internal/ports"
20+ "bnema.dev/ermite/internal/ports/mocks"
21+)
22+
23+func asyncMergeServer(t *testing.T, state domain.MRState) (*Server, *mocks.MergeRequestService) {
24+ t.Helper()
25+ svc := mocks.NewMergeRequestService(t)
26+ browse := mocks.NewBrowseService(t)
27+ browse.EXPECT().ListRefs(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(ports.RefPage{Refs: []domain.Ref{{Kind: domain.RefKindBranch, ShortName: "feature"}}}, nil).Maybe()
28+ svc.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), mock.Anything).Return(ports.MergeRequestDetail{
29+ MergeRequest: domain.MergeRequest{Repo: repoName(t, "demo"), Number: 3, State: state, HeadID: objectID(t, strings.Repeat("a", domain.ObjectIDLen)), Source: "feature", Target: "main"},
30+ Mergeability: domain.Mergeability{State: domain.MergeabilityUpToDate},
31+ }, nil).Maybe()
32+ return newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "demo")), Browse: browse, MergeRequests: svc, Now: time.Now}), svc
33+}
34+
35+func TestAsyncMergePost(t *testing.T) {
36+ for _, mode := range []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge} {
37+ for _, hx := range []bool{false, true} {
38+ t.Run(mode.String()+"/"+boolText(hx), func(t *testing.T) {
39+ s, svc := asyncMergeServer(t, domain.MRStateMerged)
40+ svc.EXPECT().Merge(mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), ports.MergeParams{Mode: mode}).Return(ports.MergeResult{}, nil).Once()
41+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode="+mode.String())
42+ if hx {
43+ r.Header.Set("HX-Request", "true")
44+ }
45+ w := httptest.NewRecorder()
46+ s.mrAction(s.mrMerge)(w, r)
47+ if !hx {
48+ assert.Equal(t, http.StatusSeeOther, w.Code)
49+ return
50+ }
51+ assert.Equal(t, http.StatusOK, w.Code)
52+ assert.Empty(t, w.Header().Get("Location"))
53+ assert.Contains(t, w.Body.String(), `id="mr-state"`)
54+ assert.Contains(t, w.Body.String(), `data-state="merged"`)
55+ assert.Contains(t, w.Body.String(), `id="mr-actions"`)
56+ assert.NotContains(t, w.Body.String(), "<html")
57+ assert.NotContains(t, w.Body.String(), `id="merge-menu"`)
58+ })
59+ }
60+ }
61+}
62+
63+func TestAsyncMergeRefusal(t *testing.T) {
64+ s, svc := asyncMergeServer(t, domain.MRStateOpen)
65+ svc.EXPECT().Merge(mock.Anything, mock.Anything, mock.Anything, int64(3), mock.Anything).Return(ports.MergeResult{}, domain.ErrConflict)
66+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode=squash")
67+ r = r.WithContext(context.WithValue(r.Context(), signedInKey{}, struct{}{}))
68+ r.Header.Set("HX-Request", "true")
69+ w := httptest.NewRecorder()
70+ s.mrAction(s.mrMerge)(w, r)
71+ assert.Equal(t, http.StatusConflict, w.Code)
72+ assert.Contains(t, w.Body.String(), `role="alert"`)
73+ assert.Contains(t, w.Body.String(), domain.ErrConflict.Error())
74+ assert.Contains(t, w.Body.String(), `hx-post="/demo/merge-requests/3/merge"`)
75+ assert.Contains(t, w.Body.String(), `hx-sync="this:drop"`)
76+ assert.Contains(t, w.Body.String(), "Merging…")
77+ assert.NotContains(t, w.Body.String(), "<html")
78+}
79+
80+func TestAsyncMergeLive(t *testing.T) {
81+ s, _ := asyncMergeServer(t, domain.MRStateMerged)
82+ l := liveStream{s: s, repo: "demo", view: "mr", n: "3"}
83+ fs, err := l.repoPage(t.Context(), watchOf(domain.MergeRequestMerged{MergeRequest: domain.MergeRequest{Number: 3}}))
84+ require.NoError(t, err)
85+ require.Len(t, fs, 1)
86+ assert.Equal(t, "mr-mergeability", fs[0].event)
87+ assert.Contains(t, fs[0].html, `data-state="merged"`)
88+ assert.Contains(t, fs[0].html, `id="mr-actions"`)
89+ assert.False(t, strings.Contains(fs[0].html, "reload"))
90+ fs, err = l.repoPage(t.Context(), watchOf(domain.MergeRequestMerged{MergeRequest: domain.MergeRequest{Number: 4}}))
91+ require.NoError(t, err)
92+ assert.Empty(t, fs)
93+}
94+
95+func TestAsyncMergeOtherFailures(t *testing.T) {
96+ for _, tt := range []struct {
97+ name string
98+ mergeErr error
99+ readFails bool
100+ status int
101+ message string
102+ }{
103+ {"forbidden", domain.ErrForbidden, false, http.StatusForbidden, "the session is not allowed"},
104+ {"internal", errors.New("private backend detail"), false, http.StatusInternalServerError, "reload to check its state"},
105+ {"post-merge read", nil, true, http.StatusOK, "merged, but the updated page could not be loaded"},
106+ } {
107+ t.Run(tt.name, func(t *testing.T) {
108+ s, svc := asyncMergeServer(t, domain.MRStateMerged)
109+ if tt.readFails {
110+ svc.On("Get", mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), mock.Anything).Unset()
111+ svc.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), mock.Anything).Return(ports.MergeRequestDetail{}, errors.New("private read detail")).Once()
112+ }
113+ svc.EXPECT().Merge(mock.Anything, mock.Anything, mock.Anything, int64(3), mock.Anything).Return(ports.MergeResult{}, tt.mergeErr).Once()
114+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode=squash")
115+ r.Header.Set("HX-Request", "true")
116+ w := httptest.NewRecorder()
117+ s.mrAction(s.mrMerge)(w, r)
118+ assert.Equal(t, tt.status, w.Code)
119+ assert.Contains(t, w.Body.String(), `id="mr-actions"`)
120+ assert.Contains(t, w.Body.String(), `hx-swap-oob="true"`)
121+ assert.Contains(t, w.Body.String(), `role="alert"`)
122+ assert.Contains(t, w.Body.String(), tt.message)
123+ assert.NotContains(t, w.Body.String(), "private")
124+ assert.NotContains(t, w.Body.String(), "<html")
125+ })
126+ }
127+}
128+
129+func TestMergeGateRefusalFragment(t *testing.T) {
130+ s, _ := newSessionServer(t, newSessions(t, "tok"), testPublicURL)
131+ for _, origin := range []string{testPublicURL, "https://other.example"} {
132+ for _, hx := range []bool{false, true} {
133+ r := httptest.NewRequest(http.MethodPost, "/demo/merge-requests/3/merge", strings.NewReader("mode=squash"))
134+ r.Header.Set("Origin", origin)
135+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
136+ if hx {
137+ r.Header.Set("HX-Request", "true")
138+ }
139+ w := httptest.NewRecorder()
140+ s.ServeHTTP(w, r)
141+ assert.Equal(t, http.StatusForbidden, w.Code)
142+ if hx {
143+ assert.Contains(t, w.Body.String(), `hx-swap-oob="true"`)
144+ assert.Contains(t, w.Body.String(), `id="mr-actions"`)
145+ if origin == testPublicURL {
146+ assert.Contains(t, w.Body.String(), "your session has expired")
147+ } else {
148+ assert.Contains(t, w.Body.String(), "origin was not accepted")
149+ }
150+ } else {
151+ assert.NotContains(t, w.Body.String(), "hx-swap-oob")
152+ }
153+ }
154+ }
155+}
156+
157+func TestNonHXMergeRefusalHasOneAlert(t *testing.T) {
158+ s, svc := asyncMergeServer(t, domain.MRStateMerged)
159+ svc.EXPECT().CountOpen(mock.Anything, mock.Anything, mock.Anything).Return(0, nil)
160+ svc.EXPECT().Merge(mock.Anything, mock.Anything, mock.Anything, int64(3), mock.Anything).Return(ports.MergeResult{}, domain.ErrConflict).Once()
161+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode=squash")
162+ w := httptest.NewRecorder()
163+ s.mrAction(s.mrMerge)(w, r)
164+ assert.Equal(t, http.StatusConflict, w.Code)
165+ assert.Equal(t, 1, strings.Count(w.Body.String(), `role="alert"`))
166+}
@@ -0,0 +1,166 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "context"
7+ "errors"
8+ "net/http"
9+ "net/http/httptest"
10+ "strings"
11+ "testing"
12+ "time"
13+
14+ "github.com/stretchr/testify/assert"
15+ "github.com/stretchr/testify/mock"
16+ "github.com/stretchr/testify/require"
17+
18+ "bnema.dev/ermite/internal/domain"
19+ "bnema.dev/ermite/internal/ports"
20+ "bnema.dev/ermite/internal/ports/mocks"
21+)
22+
23+func asyncMergeServer(t *testing.T, state domain.MRState) (*Server, *mocks.MergeRequestService) {
24+ t.Helper()
25+ svc := mocks.NewMergeRequestService(t)
26+ browse := mocks.NewBrowseService(t)
27+ browse.EXPECT().ListRefs(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(ports.RefPage{Refs: []domain.Ref{{Kind: domain.RefKindBranch, ShortName: "feature"}}}, nil).Maybe()
28+ svc.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), mock.Anything).Return(ports.MergeRequestDetail{
29+ MergeRequest: domain.MergeRequest{Repo: repoName(t, "demo"), Number: 3, State: state, HeadID: objectID(t, strings.Repeat("a", domain.ObjectIDLen)), Source: "feature", Target: "main"},
30+ Mergeability: domain.Mergeability{State: domain.MergeabilityUpToDate},
31+ }, nil).Maybe()
32+ return newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "demo")), Browse: browse, MergeRequests: svc, Now: time.Now}), svc
33+}
34+
35+func TestAsyncMergePost(t *testing.T) {
36+ for _, mode := range []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge} {
37+ for _, hx := range []bool{false, true} {
38+ t.Run(mode.String()+"/"+boolText(hx), func(t *testing.T) {
39+ s, svc := asyncMergeServer(t, domain.MRStateMerged)
40+ svc.EXPECT().Merge(mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), ports.MergeParams{Mode: mode}).Return(ports.MergeResult{}, nil).Once()
41+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode="+mode.String())
42+ if hx {
43+ r.Header.Set("HX-Request", "true")
44+ }
45+ w := httptest.NewRecorder()
46+ s.mrAction(s.mrMerge)(w, r)
47+ if !hx {
48+ assert.Equal(t, http.StatusSeeOther, w.Code)
49+ return
50+ }
51+ assert.Equal(t, http.StatusOK, w.Code)
52+ assert.Empty(t, w.Header().Get("Location"))
53+ assert.Contains(t, w.Body.String(), `id="mr-state"`)
54+ assert.Contains(t, w.Body.String(), `data-state="merged"`)
55+ assert.Contains(t, w.Body.String(), `id="mr-actions"`)
56+ assert.NotContains(t, w.Body.String(), "<html")
57+ assert.NotContains(t, w.Body.String(), `id="merge-menu"`)
58+ })
59+ }
60+ }
61+}
62+
63+func TestAsyncMergeRefusal(t *testing.T) {
64+ s, svc := asyncMergeServer(t, domain.MRStateOpen)
65+ svc.EXPECT().Merge(mock.Anything, mock.Anything, mock.Anything, int64(3), mock.Anything).Return(ports.MergeResult{}, domain.ErrConflict)
66+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode=squash")
67+ r = r.WithContext(context.WithValue(r.Context(), signedInKey{}, struct{}{}))
68+ r.Header.Set("HX-Request", "true")
69+ w := httptest.NewRecorder()
70+ s.mrAction(s.mrMerge)(w, r)
71+ assert.Equal(t, http.StatusConflict, w.Code)
72+ assert.Contains(t, w.Body.String(), `role="alert"`)
73+ assert.Contains(t, w.Body.String(), domain.ErrConflict.Error())
74+ assert.Contains(t, w.Body.String(), `hx-post="/demo/merge-requests/3/merge"`)
75+ assert.Contains(t, w.Body.String(), `hx-sync="this:drop"`)
76+ assert.Contains(t, w.Body.String(), "Merging…")
77+ assert.NotContains(t, w.Body.String(), "<html")
78+}
79+
80+func TestAsyncMergeLive(t *testing.T) {
81+ s, _ := asyncMergeServer(t, domain.MRStateMerged)
82+ l := liveStream{s: s, repo: "demo", view: "mr", n: "3"}
83+ fs, err := l.repoPage(t.Context(), watchOf(domain.MergeRequestMerged{MergeRequest: domain.MergeRequest{Number: 3}}))
84+ require.NoError(t, err)
85+ require.Len(t, fs, 1)
86+ assert.Equal(t, "mr-mergeability", fs[0].event)
87+ assert.Contains(t, fs[0].html, `data-state="merged"`)
88+ assert.Contains(t, fs[0].html, `id="mr-actions"`)
89+ assert.False(t, strings.Contains(fs[0].html, "reload"))
90+ fs, err = l.repoPage(t.Context(), watchOf(domain.MergeRequestMerged{MergeRequest: domain.MergeRequest{Number: 4}}))
91+ require.NoError(t, err)
92+ assert.Empty(t, fs)
93+}
94+
95+func TestAsyncMergeOtherFailures(t *testing.T) {
96+ for _, tt := range []struct {
97+ name string
98+ mergeErr error
99+ readFails bool
100+ status int
101+ message string
102+ }{
103+ {"forbidden", domain.ErrForbidden, false, http.StatusForbidden, "the session is not allowed"},
104+ {"internal", errors.New("private backend detail"), false, http.StatusInternalServerError, "reload to check its state"},
105+ {"post-merge read", nil, true, http.StatusOK, "merged, but the updated page could not be loaded"},
106+ } {
107+ t.Run(tt.name, func(t *testing.T) {
108+ s, svc := asyncMergeServer(t, domain.MRStateMerged)
109+ if tt.readFails {
110+ svc.On("Get", mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), mock.Anything).Unset()
111+ svc.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "demo"), int64(3), mock.Anything).Return(ports.MergeRequestDetail{}, errors.New("private read detail")).Once()
112+ }
113+ svc.EXPECT().Merge(mock.Anything, mock.Anything, mock.Anything, int64(3), mock.Anything).Return(ports.MergeResult{}, tt.mergeErr).Once()
114+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode=squash")
115+ r.Header.Set("HX-Request", "true")
116+ w := httptest.NewRecorder()
117+ s.mrAction(s.mrMerge)(w, r)
118+ assert.Equal(t, tt.status, w.Code)
119+ assert.Contains(t, w.Body.String(), `id="mr-actions"`)
120+ assert.Contains(t, w.Body.String(), `hx-swap-oob="true"`)
121+ assert.Contains(t, w.Body.String(), `role="alert"`)
122+ assert.Contains(t, w.Body.String(), tt.message)
123+ assert.NotContains(t, w.Body.String(), "private")
124+ assert.NotContains(t, w.Body.String(), "<html")
125+ })
126+ }
127+}
128+
129+func TestMergeGateRefusalFragment(t *testing.T) {
130+ s, _ := newSessionServer(t, newSessions(t, "tok"), testPublicURL)
131+ for _, origin := range []string{testPublicURL, "https://other.example"} {
132+ for _, hx := range []bool{false, true} {
133+ r := httptest.NewRequest(http.MethodPost, "/demo/merge-requests/3/merge", strings.NewReader("mode=squash"))
134+ r.Header.Set("Origin", origin)
135+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
136+ if hx {
137+ r.Header.Set("HX-Request", "true")
138+ }
139+ w := httptest.NewRecorder()
140+ s.ServeHTTP(w, r)
141+ assert.Equal(t, http.StatusForbidden, w.Code)
142+ if hx {
143+ assert.Contains(t, w.Body.String(), `hx-swap-oob="true"`)
144+ assert.Contains(t, w.Body.String(), `id="mr-actions"`)
145+ if origin == testPublicURL {
146+ assert.Contains(t, w.Body.String(), "your session has expired")
147+ } else {
148+ assert.Contains(t, w.Body.String(), "origin was not accepted")
149+ }
150+ } else {
151+ assert.NotContains(t, w.Body.String(), "hx-swap-oob")
152+ }
153+ }
154+ }
155+}
156+
157+func TestNonHXMergeRefusalHasOneAlert(t *testing.T) {
158+ s, svc := asyncMergeServer(t, domain.MRStateMerged)
159+ svc.EXPECT().CountOpen(mock.Anything, mock.Anything, mock.Anything).Return(0, nil)
160+ svc.EXPECT().Merge(mock.Anything, mock.Anything, mock.Anything, int64(3), mock.Anything).Return(ports.MergeResult{}, domain.ErrConflict).Once()
161+ r := autoMergeRequest("/demo/merge-requests/3/merge", "mode=squash")
162+ w := httptest.NewRecorder()
163+ s.mrAction(s.mrMerge)(w, r)
164+ assert.Equal(t, http.StatusConflict, w.Code)
165+ assert.Equal(t, 1, strings.Count(w.Body.String(), `role="alert"`))
166+}
M internal/adapters/web/mr_post.go +33 -1
@@ -68,6 +68,20 @@ func (s *Server) mrAction(h func(ctx context.Context, r *http.Request) (to strin
6868 s.actionFailed(ctx, w, r, err, back)
6969 return
7070 }
71+ if isHXMerge(r) {
72+ c, _, err := s.mrPageView(ctx, r, false, true)
73+ if err != nil {
74+ id := requestID()
75+ log := zerowrap.FromCtx(ctx)
76+ log.Error().Err(err).Str("request_id", id).Msg("web merged state failed")
77+ w.Header().Set("Cache-Control", "no-store")
78+ s.write(ctx, w, r, http.StatusOK, MRActionStatus("merged, but the updated page could not be loaded. reload to see the result. reference: "+id, back))
79+ return
80+ }
81+ w.Header().Set("Cache-Control", "no-store")
82+ s.write(ctx, w, r, http.StatusOK, c)
83+ return
84+ }
7185 seeOther(w, r, to)
7286 }
7387 }
@@ -100,7 +114,7 @@ func mergeErrorOf(ctx context.Context) string {
100114 // It reports false when the page cannot be built; the caller then answers as
101115 // for any refused action.
102116 func (s *Server) rerenderMR(ctx context.Context, w http.ResponseWriter, r *http.Request, re *rerender) bool {
103- c, _, err := s.mrPage(withMergeError(ctx, re.msg), r, false)
117+ c, _, err := s.mrPageView(withMergeError(ctx, re.msg), r, false, isHXMerge(r))
104118 if err != nil {
105119 return false
106120 }
@@ -120,6 +134,10 @@ func (s *Server) actionFailed(ctx context.Context, w http.ResponseWriter, r *htt
120134 // The browser is gone.
121135 case errors.Is(err, domain.ErrNotFound):
122136 w.Header().Set("Cache-Control", "no-store")
137+ if isHXMerge(r) {
138+ s.write(ctx, w, r, http.StatusNotFound, MRActionStatus("the merge request was not found. reload to check its state.", back))
139+ return
140+ }
123141 s.write(ctx, w, r, http.StatusNotFound, s.notFoundPage())
124142 case errors.Is(err, domain.ErrAlreadyExists):
125143 s.reject(ctx, w, r, http.StatusConflict, "already there", rule(msg, "this already exists."), back)
@@ -143,6 +161,10 @@ func (s *Server) internalFailure(ctx context.Context, w http.ResponseWriter, r *
143161 log := zerowrap.FromCtx(ctx)
144162 log.Error().Err(err).Str("request_id", id).Str("path", logPath(r.URL.Path)).Msg("web action failed")
145163 w.Header().Set("Cache-Control", "no-store")
164+ if isHXMerge(r) {
165+ s.write(ctx, w, r, http.StatusInternalServerError, MRActionStatus("the merge could not be completed. reload to check its state. reference: "+id, strings.TrimSuffix(r.URL.Path, "/merge")))
166+ return
167+ }
146168 s.write(ctx, w, r, http.StatusInternalServerError, s.errorPage(id))
147169 }
148170
@@ -161,6 +183,11 @@ func (s *Server) reject(ctx context.Context, w http.ResponseWriter, r *http.Requ
161183 if back == "" {
162184 back = "/"
163185 }
186+ if isHXMerge(r) {
187+ w.Header().Set("Cache-Control", "no-store")
188+ s.write(ctx, w, r, status, MRActionStatus(line, back))
189+ return
190+ }
164191 l := s.stateLayout(title, []Crumb{{Label: title, Current: true}})
165192 w.Header().Set("Cache-Control", "no-store")
166193 s.write(ctx, w, r, status, State(l, StatePage{
@@ -523,3 +550,8 @@ func (s *Server) mrDeleteComment(ctx context.Context, r *http.Request) (string,
523550 c, err := s.cfg.MergeRequests.DeleteComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")))
524551 return back + "#c" + strconv.FormatInt(c.ID, 10), back, err
525552 }
553+
554+// isHXMerge limits fragment responses to the progressively enhanced merge form.
555+func isHXMerge(r *http.Request) bool {
556+ return r.Header.Get("HX-Request") == "true" && strings.HasSuffix(r.URL.Path, "/merge")
557+}
@@ -68,6 +68,20 @@ func (s *Server) mrAction(h func(ctx context.Context, r *http.Request) (to strin
68 s.actionFailed(ctx, w, r, err, back)68 s.actionFailed(ctx, w, r, err, back)
69 return69 return
70 }70 }
71+ if isHXMerge(r) {
72+ c, _, err := s.mrPageView(ctx, r, false, true)
73+ if err != nil {
74+ id := requestID()
75+ log := zerowrap.FromCtx(ctx)
76+ log.Error().Err(err).Str("request_id", id).Msg("web merged state failed")
77+ w.Header().Set("Cache-Control", "no-store")
78+ s.write(ctx, w, r, http.StatusOK, MRActionStatus("merged, but the updated page could not be loaded. reload to see the result. reference: "+id, back))
79+ return
80+ }
81+ w.Header().Set("Cache-Control", "no-store")
82+ s.write(ctx, w, r, http.StatusOK, c)
83+ return
84+ }
71 seeOther(w, r, to)85 seeOther(w, r, to)
72 }86 }
73 }87 }
@@ -100,7 +114,7 @@ func mergeErrorOf(ctx context.Context) string {
100 // It reports false when the page cannot be built; the caller then answers as114 // It reports false when the page cannot be built; the caller then answers as
101 // for any refused action.115 // for any refused action.
102 func (s *Server) rerenderMR(ctx context.Context, w http.ResponseWriter, r *http.Request, re *rerender) bool {116 func (s *Server) rerenderMR(ctx context.Context, w http.ResponseWriter, r *http.Request, re *rerender) bool {
103- c, _, err := s.mrPage(withMergeError(ctx, re.msg), r, false)117+ c, _, err := s.mrPageView(withMergeError(ctx, re.msg), r, false, isHXMerge(r))
104 if err != nil {118 if err != nil {
105 return false119 return false
106 }120 }
@@ -120,6 +134,10 @@ func (s *Server) actionFailed(ctx context.Context, w http.ResponseWriter, r *htt
120 // The browser is gone.134 // The browser is gone.
121 case errors.Is(err, domain.ErrNotFound):135 case errors.Is(err, domain.ErrNotFound):
122 w.Header().Set("Cache-Control", "no-store")136 w.Header().Set("Cache-Control", "no-store")
137+ if isHXMerge(r) {
138+ s.write(ctx, w, r, http.StatusNotFound, MRActionStatus("the merge request was not found. reload to check its state.", back))
139+ return
140+ }
123 s.write(ctx, w, r, http.StatusNotFound, s.notFoundPage())141 s.write(ctx, w, r, http.StatusNotFound, s.notFoundPage())
124 case errors.Is(err, domain.ErrAlreadyExists):142 case errors.Is(err, domain.ErrAlreadyExists):
125 s.reject(ctx, w, r, http.StatusConflict, "already there", rule(msg, "this already exists."), back)143 s.reject(ctx, w, r, http.StatusConflict, "already there", rule(msg, "this already exists."), back)
@@ -143,6 +161,10 @@ func (s *Server) internalFailure(ctx context.Context, w http.ResponseWriter, r *
143 log := zerowrap.FromCtx(ctx)161 log := zerowrap.FromCtx(ctx)
144 log.Error().Err(err).Str("request_id", id).Str("path", logPath(r.URL.Path)).Msg("web action failed")162 log.Error().Err(err).Str("request_id", id).Str("path", logPath(r.URL.Path)).Msg("web action failed")
145 w.Header().Set("Cache-Control", "no-store")163 w.Header().Set("Cache-Control", "no-store")
164+ if isHXMerge(r) {
165+ s.write(ctx, w, r, http.StatusInternalServerError, MRActionStatus("the merge could not be completed. reload to check its state. reference: "+id, strings.TrimSuffix(r.URL.Path, "/merge")))
166+ return
167+ }
146 s.write(ctx, w, r, http.StatusInternalServerError, s.errorPage(id))168 s.write(ctx, w, r, http.StatusInternalServerError, s.errorPage(id))
147 }169 }
148 170
@@ -161,6 +183,11 @@ func (s *Server) reject(ctx context.Context, w http.ResponseWriter, r *http.Requ
161 if back == "" {183 if back == "" {
162 back = "/"184 back = "/"
163 }185 }
186+ if isHXMerge(r) {
187+ w.Header().Set("Cache-Control", "no-store")
188+ s.write(ctx, w, r, status, MRActionStatus(line, back))
189+ return
190+ }
164 l := s.stateLayout(title, []Crumb{{Label: title, Current: true}})191 l := s.stateLayout(title, []Crumb{{Label: title, Current: true}})
165 w.Header().Set("Cache-Control", "no-store")192 w.Header().Set("Cache-Control", "no-store")
166 s.write(ctx, w, r, status, State(l, StatePage{193 s.write(ctx, w, r, status, State(l, StatePage{
@@ -523,3 +550,8 @@ func (s *Server) mrDeleteComment(ctx context.Context, r *http.Request) (string,
523 c, err := s.cfg.MergeRequests.DeleteComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")))550 c, err := s.cfg.MergeRequests.DeleteComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")))
524 return back + "#c" + strconv.FormatInt(c.ID, 10), back, err551 return back + "#c" + strconv.FormatInt(c.ID, 10), back, err
525 }552 }
553+
554+// isHXMerge limits fragment responses to the progressively enhanced merge form.
555+func isHXMerge(r *http.Request) bool {
556+ return r.Header.Get("HX-Request") == "true" && strings.HasSuffix(r.URL.Path, "/merge")
557+}
M internal/adapters/web/mr_templ.go +1226 -1034 generated file: collapsed
M internal/adapters/web/overlays.templ +0 -3
@@ -27,9 +27,6 @@ templ OverlayHelp(mr bool) {
2727 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>
2828 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
2929 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>
30- if SignedIn(ctx) {
31- <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
32- }
3330 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
3431 <div><dt><kbd>w</kbd></dt><dd>wide mode: use the whole width of the screen, or not (remembered)</dd></div>
3532 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
@@ -27,9 +27,6 @@ templ OverlayHelp(mr bool) {
27 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>27 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>
28 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>28 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
29 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>29 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>
30- if SignedIn(ctx) {
31- <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
32- }
33 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>30 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
34 <div><dt><kbd>w</kbd></dt><dd>wide mode: use the whole width of the screen, or not (remembered)</dd></div>31 <div><dt><kbd>w</kbd></dt><dd>wide mode: use the whole width of the screen, or not (remembered)</dd></div>
35 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>32 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
M internal/adapters/web/overlays_templ.go +39 -49 generated file: collapsed
M internal/adapters/web/session.go +12 -2
@@ -178,11 +178,11 @@ func (s *Server) gate(w http.ResponseWriter, r *http.Request) bool {
178178 // Host header, which the client chooses. A missing header, "null" and any
179179 // other origin are refused.
180180 if r.Header.Get("Origin") != s.origin {
181- denyForbidden(w)
181+ s.denyMergeGate(w, r, "the request origin was not accepted. reload this page and try again.")
182182 return false
183183 }
184184 if !SignedIn(r.Context()) && !s.anonymousPost(r) {
185- denyForbidden(w)
185+ s.denyMergeGate(w, r, "your session has expired. sign in again and reload.")
186186 return false
187187 }
188188 return true
@@ -335,3 +335,13 @@ func (s *Server) loginRejectedPage() templ.Component {
335335 return s.stateOnly("sign in", "404", "login link not valid",
336336 "this link has expired, was already used, or never existed.", "run `ermite web login` for a new one.")
337337 }
338+
339+// denyMergeGate keeps gate refusals visible to the enhanced merge form.
340+func (s *Server) denyMergeGate(w http.ResponseWriter, r *http.Request, message string) {
341+ if !isHXMerge(r) {
342+ denyForbidden(w)
343+ return
344+ }
345+ w.Header().Set("Cache-Control", "no-store")
346+ s.write(r.Context(), w, r, http.StatusForbidden, MRActionStatus(message, strings.TrimSuffix(r.URL.Path, "/merge")))
347+}
@@ -178,11 +178,11 @@ func (s *Server) gate(w http.ResponseWriter, r *http.Request) bool {
178 // Host header, which the client chooses. A missing header, "null" and any178 // Host header, which the client chooses. A missing header, "null" and any
179 // other origin are refused.179 // other origin are refused.
180 if r.Header.Get("Origin") != s.origin {180 if r.Header.Get("Origin") != s.origin {
181- denyForbidden(w)181+ s.denyMergeGate(w, r, "the request origin was not accepted. reload this page and try again.")
182 return false182 return false
183 }183 }
184 if !SignedIn(r.Context()) && !s.anonymousPost(r) {184 if !SignedIn(r.Context()) && !s.anonymousPost(r) {
185- denyForbidden(w)185+ s.denyMergeGate(w, r, "your session has expired. sign in again and reload.")
186 return false186 return false
187 }187 }
188 return true188 return true
@@ -335,3 +335,13 @@ func (s *Server) loginRejectedPage() templ.Component {
335 return s.stateOnly("sign in", "404", "login link not valid",335 return s.stateOnly("sign in", "404", "login link not valid",
336 "this link has expired, was already used, or never existed.", "run `ermite web login` for a new one.")336 "this link has expired, was already used, or never existed.", "run `ermite web login` for a new one.")
337 }337 }
338+
339+// denyMergeGate keeps gate refusals visible to the enhanced merge form.
340+func (s *Server) denyMergeGate(w http.ResponseWriter, r *http.Request, message string) {
341+ if !isHXMerge(r) {
342+ denyForbidden(w)
343+ return
344+ }
345+ w.Header().Set("Cache-Control", "no-store")
346+ s.write(r.Context(), w, r, http.StatusForbidden, MRActionStatus(message, strings.TrimSuffix(r.URL.Path, "/merge")))
347+}
M internal/adapters/web/shell.templ +1 -5
@@ -67,9 +67,6 @@ templ Shell(l Layout, rail templ.Component) {
6767 </div>
6868 @Footer(l)
6969 @Hint(l)
70- if SignedIn(ctx) {
71- <a hidden data-inbox href={ inboxPath }>Inbox</a>
72- }
7370 @OverlayHelp(l.MR)
7471 @OverlayRefs(l)
7572 @OverlayCmd(l.Cmd)
@@ -90,8 +87,7 @@ templ Header(l Layout) {
9087 <a
9188 class="btn inbox-btn"
9289 href={ templ.SafeURL(l.Inbox.Href) }
93- title="inbox (I)"
94- aria-keyshortcuts="Shift+I"
90+ title="inbox"
9591 >
9692 <span class="ico i-mailbox" aria-hidden="true"></span>
9793 Inbox
@@ -67,9 +67,6 @@ templ Shell(l Layout, rail templ.Component) {
67 </div>67 </div>
68 @Footer(l)68 @Footer(l)
69 @Hint(l)69 @Hint(l)
70- if SignedIn(ctx) {
71- <a hidden data-inbox href={ inboxPath }>Inbox</a>
72- }
73 @OverlayHelp(l.MR)70 @OverlayHelp(l.MR)
74 @OverlayRefs(l)71 @OverlayRefs(l)
75 @OverlayCmd(l.Cmd)72 @OverlayCmd(l.Cmd)
@@ -90,8 +87,7 @@ templ Header(l Layout) {
90 <a87 <a
91 class="btn inbox-btn"88 class="btn inbox-btn"
92 href={ templ.SafeURL(l.Inbox.Href) }89 href={ templ.SafeURL(l.Inbox.Href) }
93- title="inbox (I)"90+ title="inbox"
94- aria-keyshortcuts="Shift+I"
95 >91 >
96 <span class="ico i-mailbox" aria-hidden="true"></span>92 <span class="ico i-mailbox" aria-hidden="true"></span>
97 Inbox93 Inbox
M internal/adapters/web/shell_templ.go +133 -152 generated file: collapsed
M internal/adapters/web/static/css/60-mr.css +2 -0
@@ -1,6 +1,8 @@
11 @layer components {
22 /* merge requests: the review page. Hue only where it carries a verdict. */
33 .mr-h .show-kv { margin-top: 0; }
4+ #merge-menu .htmx-indicator { display: none; }
5+ #merge-menu .htmx-request .htmx-indicator { display: inline; }
46 /* one card: the header (title, facts) a shade lighter, the description below */
57 .mr-card {
68 background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden;
@@ -1,6 +1,8 @@
1 @layer components {1 @layer components {
2 /* merge requests: the review page. Hue only where it carries a verdict. */2 /* merge requests: the review page. Hue only where it carries a verdict. */
3 .mr-h .show-kv { margin-top: 0; }3 .mr-h .show-kv { margin-top: 0; }
4+ #merge-menu .htmx-indicator { display: none; }
5+ #merge-menu .htmx-request .htmx-indicator { display: inline; }
4 /* one card: the header (title, facts) a shade lighter, the description below */6 /* one card: the header (title, facts) a shade lighter, the description below */
5 .mr-card {7 .mr-card {
6 background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden;8 background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden;
M internal/adapters/web/static/js/ermite.js +2 -2
@@ -612,7 +612,7 @@
612612
613613 // 1. Keys.
614614 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
615- s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', I: 'inbox', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
615+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
616616 // the merge request page (mr.js): the merge menu, mark read
617617 M: 'merge', R: 'read' };
618618 // "g t" is the key "t": the tabs have one event each.
@@ -654,7 +654,7 @@
654654 // page-level keys, as on the other lists.
655655 if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; }
656656 const name = single[e.key];
657- if (!name || (name === 'inbox' && !$('[data-inbox]'))) return;
657+ if (!name) return;
658658 if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault();
659659 ermite.fire(name);
660660 });
@@ -612,7 +612,7 @@
612 612
613 // 1. Keys.613 // 1. Keys.
614 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',614 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
615- s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', I: 'inbox', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',615+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
616 // the merge request page (mr.js): the merge menu, mark read616 // the merge request page (mr.js): the merge menu, mark read
617 M: 'merge', R: 'read' };617 M: 'merge', R: 'read' };
618 // "g t" is the key "t": the tabs have one event each.618 // "g t" is the key "t": the tabs have one event each.
@@ -654,7 +654,7 @@
654 // page-level keys, as on the other lists.654 // page-level keys, as on the other lists.
655 if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; }655 if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; }
656 const name = single[e.key];656 const name = single[e.key];
657- if (!name || (name === 'inbox' && !$('[data-inbox]'))) return;657+ if (!name) return;
658 if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault();658 if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault();
659 ermite.fire(name);659 ermite.fire(name);
660 });660 });
M internal/adapters/web/static/js/mr.js +3 -3
M internal/app/forge.go +1 -1
M internal/app/testdata/web/blob-binary.html +0 -1
M internal/app/testdata/web/blob-code.html +0 -1
M internal/app/testdata/web/blob-empty.html +0 -1
M internal/app/testdata/web/blob-image.html +0 -1
M internal/app/testdata/web/blob-markdown.html +0 -1
M internal/app/testdata/web/blob-svg.html +0 -1
M internal/app/testdata/web/blob-text.html +0 -1
M internal/app/testdata/web/blob-too-large.html +0 -1
M internal/app/testdata/web/blob-trojan.html +0 -1
M internal/app/testdata/web/commit-big.html +0 -1
M internal/app/testdata/web/commit-binary.html +0 -1
M internal/app/testdata/web/commit-merge.html +0 -1
M internal/app/testdata/web/commit-modify-split.html +0 -1
M internal/app/testdata/web/commit-modify.html +0 -1
M internal/app/testdata/web/commit-rename.html +0 -1
M internal/app/testdata/web/commit-root.html +0 -1
M internal/app/testdata/web/commit-trojan.html +0 -1
M internal/app/testdata/web/commits-page2.html +0 -1
M internal/app/testdata/web/commits-path.html +0 -1
M internal/app/testdata/web/commits-root.html +0 -1
M internal/app/testdata/web/commits-small.html +0 -1
M internal/app/testdata/web/empty.html +0 -1
M internal/app/testdata/web/home.html +0 -1
M internal/app/testdata/web/not-found-page.html +0 -1
M internal/app/testdata/web/not-found.html +0 -1
M internal/app/testdata/web/refs.html +0 -1
M internal/app/testdata/web/summary.html +0 -1
M internal/app/testdata/web/tree-nested.html +0 -1
M internal/app/testdata/web/tree-root.html +0 -1
M internal/app/testdata/web/tree-slash-ref.html +0 -1
M internal/app/testdata/web/tree-subdir.html +0 -1
M internal/app/web_inbox_test.go +2 -32
M internal/ports/dto.go +3 -6
M internal/usecase/events.go +2 -2
M internal/usecase/inbox.go +2 -31
M internal/usecase/inbox_test.go +0 -33
M skills/ermite/SKILL.md +0 -13

Comments0 open / 0

no comments yet.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 2

Tags 42

↑↓ moveenter openesc close